diff --git a/EtchBendLines b/EtchBendLines index 89d987f..da4d322 160000 --- a/EtchBendLines +++ b/EtchBendLines @@ -1 +1 @@ -Subproject commit 89d987f6c6923b458217017300789ea956114972 +Subproject commit da4d3228b0eede73a665b0a814f7b8ac818bcc94 diff --git a/ExportDXF.sln b/ExportDXF.sln index 24040fd..e194613 100644 --- a/ExportDXF.sln +++ b/ExportDXF.sln @@ -9,24 +9,94 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLi 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}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Api", "FabWorks.Api\FabWorks.Api.csproj", "{9BD571FA-52D8-430D-8843-FEB6EABD421C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x64.ActiveCfg = Debug|Any CPU + {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x64.Build.0 = Debug|Any CPU + {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x86.ActiveCfg = Debug|Any CPU + {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x86.Build.0 = Debug|Any CPU {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|Any CPU.ActiveCfg = Release|Any CPU {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|Any CPU.Build.0 = Release|Any CPU + {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x64.ActiveCfg = Release|Any CPU + {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x64.Build.0 = Release|Any CPU + {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x86.ActiveCfg = Release|Any CPU + {05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x86.Build.0 = Release|Any CPU {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x64.Build.0 = Debug|Any CPU + {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x86.Build.0 = Debug|Any CPU {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|Any CPU.Build.0 = Release|Any CPU + {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.ActiveCfg = Release|Any CPU + {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 + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x64.Build.0 = Debug|Any CPU + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x86.Build.0 = Debug|Any CPU + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|Any CPU.Build.0 = Release|Any CPU + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x64.ActiveCfg = Release|Any CPU + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x64.Build.0 = Release|Any CPU + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x86.ActiveCfg = Release|Any CPU + {24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x86.Build.0 = Release|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x64.ActiveCfg = Debug|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x64.Build.0 = Debug|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x86.ActiveCfg = Debug|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x86.Build.0 = Debug|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Release|Any CPU.Build.0 = Release|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x64.ActiveCfg = Release|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x64.Build.0 = Release|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x86.ActiveCfg = Release|Any CPU + {6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x86.Build.0 = Release|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x64.ActiveCfg = Debug|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x64.Build.0 = Debug|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x86.ActiveCfg = Debug|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x86.Build.0 = Debug|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|Any CPU.Build.0 = Release|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x64.ActiveCfg = Release|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x64.Build.0 = Release|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x86.ActiveCfg = Release|Any CPU + {9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ExportDXF/ApiClient/FabWorksApiClient.cs b/ExportDXF/ApiClient/FabWorksApiClient.cs new file mode 100644 index 0000000..e608e55 --- /dev/null +++ b/ExportDXF/ApiClient/FabWorksApiClient.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ExportDXF.ApiClient +{ + public class FabWorksApiClient : IFabWorksApiClient + { + private readonly HttpClient _http; + + public FabWorksApiClient(HttpClient httpClient) + { + _http = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + } + + public async Task CreateExportAsync(string drawingNumber, string equipmentNo, string drawingNo, string sourceFilePath, string outputFolder, string title = null) + { + var request = new ApiCreateExportRequest + { + DrawingNumber = drawingNumber, + Title = title, + EquipmentNo = equipmentNo, + DrawingNo = drawingNo, + SourceFilePath = sourceFilePath, + OutputFolder = outputFolder + }; + + var response = await _http.PostAsJsonAsync("api/exports", request); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task GetExportBySourceFileAsync(string filePath) + { + var response = await _http.GetAsync($"api/exports/by-source?path={Uri.EscapeDataString(filePath)}"); + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task> GetDrawingNumbersAsync() + { + var response = await _http.GetAsync("api/exports/drawing-numbers"); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync>(); + } + + public async Task> GetEquipmentNumbersAsync() + { + var response = await _http.GetAsync("api/exports/equipment-numbers"); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync>(); + } + + public async Task> GetDrawingNumbersByEquipmentAsync(string equipmentNo = null) + { + var url = "api/exports/drawing-numbers-by-equipment"; + if (!string.IsNullOrEmpty(equipmentNo)) + url += $"?equipmentNo={Uri.EscapeDataString(equipmentNo)}"; + + var response = await _http.GetAsync(url); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync>(); + } + + public async Task GetNextItemNumberAsync(string drawingNumber) + { + var response = await _http.GetAsync($"api/exports/next-item-number?drawingNumber={Uri.EscapeDataString(drawingNumber)}"); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + public async Task UpdatePdfHashAsync(int exportId, string pdfContentHash) + { + var request = new ApiUpdatePdfHashRequest { PdfContentHash = pdfContentHash }; + var response = await _http.PatchAsJsonAsync($"api/exports/{exportId}/pdf-hash", request); + response.EnsureSuccessStatusCode(); + } + + public async Task GetPreviousPdfHashAsync(string drawingNumber, int? excludeId = null) + { + var url = $"api/exports/previous-pdf-hash?drawingNumber={Uri.EscapeDataString(drawingNumber)}"; + if (excludeId.HasValue) + url += $"&excludeId={excludeId.Value}"; + + var response = await _http.GetAsync(url); + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + public async Task FindExistingBomItemAsync(int exportId, string partName, string configurationName) + { + var url = $"api/exports/{exportId}/bom-items/find?partName={Uri.EscapeDataString(partName ?? "")}&configurationName={Uri.EscapeDataString(configurationName ?? "")}"; + var response = await _http.GetAsync(url); + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task CreateBomItemAsync(int exportId, ApiBomItem bomItem) + { + var response = await _http.PostAsJsonAsync($"api/exports/{exportId}/bom-items", bomItem); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task GetPreviousCutTemplateAsync(string drawingNumber, string itemNo) + { + var response = await _http.GetAsync($"api/exports/previous-cut-template?drawingNumber={Uri.EscapeDataString(drawingNumber)}&itemNo={Uri.EscapeDataString(itemNo)}"); + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task UploadDxfAsync(string localFilePath, string equipment, string drawingNo, string itemNo, string contentHash) + { + using var content = new MultipartFormDataContent(); + using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read); + var fileContent = new StreamContent(fileStream); + content.Add(fileContent, "file", Path.GetFileName(localFilePath)); + content.Add(new StringContent(equipment ?? ""), "equipment"); + content.Add(new StringContent(drawingNo ?? ""), "drawingNo"); + content.Add(new StringContent(itemNo ?? ""), "itemNo"); + content.Add(new StringContent(contentHash ?? ""), "contentHash"); + + var response = await _http.PostAsync("api/files/dxf", content); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task UploadPdfAsync(string localFilePath, string equipment, string drawingNo, string contentHash, int? exportRecordId = null) + { + using var content = new MultipartFormDataContent(); + using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read); + var fileContent = new StreamContent(fileStream); + content.Add(fileContent, "file", Path.GetFileName(localFilePath)); + content.Add(new StringContent(equipment ?? ""), "equipment"); + content.Add(new StringContent(drawingNo ?? ""), "drawingNo"); + content.Add(new StringContent(contentHash ?? ""), "contentHash"); + if (exportRecordId.HasValue) + content.Add(new StringContent(exportRecordId.Value.ToString()), "exportRecordId"); + + var response = await _http.PostAsync("api/files/pdf", content); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + } +} diff --git a/ExportDXF/ApiClient/FabWorksApiDtos.cs b/ExportDXF/ApiClient/FabWorksApiDtos.cs new file mode 100644 index 0000000..5e496c0 --- /dev/null +++ b/ExportDXF/ApiClient/FabWorksApiDtos.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; + +namespace ExportDXF.ApiClient +{ + public class ApiExportDetail + { + public int Id { get; set; } + public string DrawingNumber { get; set; } + public string Title { get; set; } + public string EquipmentNo { get; set; } + public string DrawingNo { get; set; } + public string SourceFilePath { get; set; } + public string OutputFolder { get; set; } + public DateTime ExportedAt { get; set; } + public string ExportedBy { get; set; } + public string PdfContentHash { get; set; } + public List BomItems { get; set; } = new(); + } + + public class ApiBomItem + { + public int ID { get; set; } + public string ItemNo { get; set; } + public string PartNo { get; set; } + public int SortOrder { get; set; } + public int? Qty { get; set; } + public int? TotalQty { get; set; } + public string Description { get; set; } + public string PartName { get; set; } + public string ConfigurationName { get; set; } + public string Material { get; set; } + public ApiCutTemplate CutTemplate { get; set; } + public ApiFormProgram FormProgram { get; set; } + } + + public class ApiCutTemplate + { + public int Id { get; set; } + public string DxfFilePath { get; set; } + public string ContentHash { get; set; } + public int Revision { get; set; } + public double? Thickness { get; set; } + public double? KFactor { get; set; } + public double? DefaultBendRadius { get; set; } + } + + public class ApiFormProgram + { + public int Id { get; set; } + public string ProgramFilePath { get; set; } + public string ContentHash { get; set; } + public string ProgramName { get; set; } + public double? Thickness { get; set; } + public string MaterialType { get; set; } + public double? KFactor { get; set; } + public int BendCount { get; set; } + public string UpperToolNames { get; set; } + public string LowerToolNames { get; set; } + public string SetupNotes { get; set; } + } + + public class ApiCreateExportRequest + { + public string DrawingNumber { get; set; } + public string Title { get; set; } + public string EquipmentNo { get; set; } + public string DrawingNo { get; set; } + public string SourceFilePath { get; set; } + public string OutputFolder { get; set; } + } + + public class ApiUpdatePdfHashRequest + { + public string PdfContentHash { get; set; } + } + + public class ApiFileUploadResponse + { + public string StoredFilePath { get; set; } + public string ContentHash { get; set; } + public string FileName { get; set; } + public bool WasUnchanged { get; set; } + public bool IsNewFile { get; set; } + } +} diff --git a/ExportDXF/ApiClient/IFabWorksApiClient.cs b/ExportDXF/ApiClient/IFabWorksApiClient.cs new file mode 100644 index 0000000..45caaca --- /dev/null +++ b/ExportDXF/ApiClient/IFabWorksApiClient.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ExportDXF.ApiClient +{ + public interface IFabWorksApiClient + { + Task CreateExportAsync(string drawingNumber, string equipmentNo, string drawingNo, string sourceFilePath, string outputFolder, string title = null); + Task GetExportBySourceFileAsync(string filePath); + Task> GetDrawingNumbersAsync(); + Task> GetEquipmentNumbersAsync(); + Task> GetDrawingNumbersByEquipmentAsync(string equipmentNo = null); + Task GetNextItemNumberAsync(string drawingNumber); + Task UpdatePdfHashAsync(int exportId, string pdfContentHash); + Task GetPreviousPdfHashAsync(string drawingNumber, int? excludeId = null); + Task FindExistingBomItemAsync(int exportId, string partName, string configurationName); + Task CreateBomItemAsync(int exportId, ApiBomItem bomItem); + Task GetPreviousCutTemplateAsync(string drawingNumber, string itemNo); + Task UploadDxfAsync(string localFilePath, string equipment, string drawingNo, string itemNo, string contentHash); + Task UploadPdfAsync(string localFilePath, string equipment, string drawingNo, string contentHash, int? exportRecordId = null); + } +} diff --git a/ExportDXF/DrawingInfo.cs b/ExportDXF/DrawingInfo.cs index 19a7863..c05083f 100644 --- a/ExportDXF/DrawingInfo.cs +++ b/ExportDXF/DrawingInfo.cs @@ -1,10 +1,11 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace ExportDXF { public class DrawingInfo { private static Regex drawingFormatRegex = new Regex(@"(?[345]\d{3}(-\d+\w{1,2})?)\s?(?[ABEP]\d+(-?(\d+[A-Z]?))?)", RegexOptions.IgnoreCase); + private static Regex equipmentOnlyRegex = new Regex(@"^(?[345]\d{3}(-\d+\w{1,2})?)\b", RegexOptions.IgnoreCase); public string EquipmentNo { get; set; } @@ -14,6 +15,8 @@ namespace ExportDXF public override string ToString() { + if (string.IsNullOrEmpty(DrawingNo)) + return EquipmentNo ?? string.Empty; return $"{EquipmentNo} {DrawingNo}"; } @@ -35,7 +38,21 @@ namespace ExportDXF var match = drawingFormatRegex.Match(input); if (match.Success == false) + { + // Try matching just the equipment number (e.g. "5028 Prox switch bracket") + var eqMatch = equipmentOnlyRegex.Match(input); + if (eqMatch.Success) + { + return new DrawingInfo + { + EquipmentNo = eqMatch.Groups["equipmentNo"].Value, + DrawingNo = null, + Source = input + }; + } + return null; + } var dwg = new DrawingInfo(); @@ -46,4 +63,4 @@ namespace ExportDXF return dwg; } } -} \ No newline at end of file +} diff --git a/ExportDXF/ExportDXF.csproj b/ExportDXF/ExportDXF.csproj index 4a45d38..fd1c36f 100644 --- a/ExportDXF/ExportDXF.csproj +++ b/ExportDXF/ExportDXF.csproj @@ -14,11 +14,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/ExportDXF/Forms/MainForm.Designer.cs b/ExportDXF/Forms/MainForm.Designer.cs index f3344c2..bc17a3e 100644 --- a/ExportDXF/Forms/MainForm.Designer.cs +++ b/ExportDXF/Forms/MainForm.Designer.cs @@ -42,6 +42,8 @@ namespace ExportDXF.Forms label1 = new System.Windows.Forms.Label(); label2 = new System.Windows.Forms.Label(); drawingNoBox = new System.Windows.Forms.ComboBox(); + titleLabel = new System.Windows.Forms.Label(); + titleBox = new System.Windows.Forms.TextBox(); mainTabControl.SuspendLayout(); logEventsTab.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)logEventsDataGrid).BeginInit(); @@ -54,10 +56,10 @@ namespace ExportDXF.Forms // runButton // runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; - runButton.Location = new System.Drawing.Point(514, 13); + runButton.Location = new System.Drawing.Point(508, 12); runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); runButton.Name = "runButton"; - runButton.Size = new System.Drawing.Size(100, 30); + runButton.Size = new System.Drawing.Size(65, 87); runButton.TabIndex = 11; runButton.Text = "Start"; runButton.UseVisualStyleBackColor = true; @@ -66,7 +68,7 @@ namespace ExportDXF.Forms // label3 // label3.AutoSize = true; - label3.Location = new System.Drawing.Point(26, 46); + label3.Location = new System.Drawing.Point(26, 77); label3.Name = "label3"; label3.Size = new System.Drawing.Size(105, 17); label3.TabIndex = 2; @@ -76,7 +78,7 @@ namespace ExportDXF.Forms // viewFlipDeciderBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; viewFlipDeciderBox.FormattingEnabled = true; - viewFlipDeciderBox.Location = new System.Drawing.Point(137, 43); + viewFlipDeciderBox.Location = new System.Drawing.Point(137, 74); viewFlipDeciderBox.Name = "viewFlipDeciderBox"; viewFlipDeciderBox.Size = new System.Drawing.Size(365, 25); viewFlipDeciderBox.TabIndex = 3; @@ -87,11 +89,11 @@ namespace ExportDXF.Forms mainTabControl.Controls.Add(logEventsTab); mainTabControl.Controls.Add(bomTab); mainTabControl.Controls.Add(cutTemplatesTab); - mainTabControl.Location = new System.Drawing.Point(15, 74); + mainTabControl.Location = new System.Drawing.Point(15, 105); mainTabControl.Name = "mainTabControl"; mainTabControl.Padding = new System.Drawing.Point(20, 5); mainTabControl.SelectedIndex = 0; - mainTabControl.Size = new System.Drawing.Size(599, 330); + mainTabControl.Size = new System.Drawing.Size(910, 492); mainTabControl.TabIndex = 12; // // logEventsTab @@ -100,7 +102,7 @@ namespace ExportDXF.Forms logEventsTab.Location = new System.Drawing.Point(4, 30); logEventsTab.Name = "logEventsTab"; logEventsTab.Padding = new System.Windows.Forms.Padding(3); - logEventsTab.Size = new System.Drawing.Size(591, 296); + logEventsTab.Size = new System.Drawing.Size(902, 458); logEventsTab.TabIndex = 0; logEventsTab.Text = "Log Events"; logEventsTab.UseVisualStyleBackColor = true; @@ -112,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(579, 282); + logEventsDataGrid.Size = new System.Drawing.Size(890, 440); logEventsDataGrid.TabIndex = 0; // // bomTab // bomTab.Controls.Add(bomDataGrid); - bomTab.Location = new System.Drawing.Point(4, 30); + bomTab.Location = new System.Drawing.Point(4, 28); bomTab.Name = "bomTab"; bomTab.Padding = new System.Windows.Forms.Padding(3); - bomTab.Size = new System.Drawing.Size(982, 549); + bomTab.Size = new System.Drawing.Size(902, 409); bomTab.TabIndex = 1; bomTab.Text = "Bill Of Materials"; bomTab.UseVisualStyleBackColor = true; @@ -133,30 +135,30 @@ 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(970, 535); + bomDataGrid.Size = new System.Drawing.Size(1281, 644); bomDataGrid.TabIndex = 1; - // + // // cutTemplatesTab - // + // cutTemplatesTab.Controls.Add(cutTemplatesDataGrid); - cutTemplatesTab.Location = new System.Drawing.Point(4, 30); + cutTemplatesTab.Location = new System.Drawing.Point(4, 28); cutTemplatesTab.Name = "cutTemplatesTab"; cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3); - cutTemplatesTab.Size = new System.Drawing.Size(982, 549); + cutTemplatesTab.Size = new System.Drawing.Size(902, 409); cutTemplatesTab.TabIndex = 2; cutTemplatesTab.Text = "Cut Templates"; cutTemplatesTab.UseVisualStyleBackColor = true; - // + // // cutTemplatesDataGrid - // + // cutTemplatesDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; cutTemplatesDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke; cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6); cutTemplatesDataGrid.Name = "cutTemplatesDataGrid"; - cutTemplatesDataGrid.Size = new System.Drawing.Size(970, 535); + cutTemplatesDataGrid.Size = new System.Drawing.Size(1281, 644); cutTemplatesDataGrid.TabIndex = 2; - // + // // equipmentBox // equipmentBox.FormattingEnabled = true; @@ -191,10 +193,28 @@ namespace ExportDXF.Forms drawingNoBox.Size = new System.Drawing.Size(119, 25); drawingNoBox.TabIndex = 13; // + // titleLabel + // + titleLabel.AutoSize = true; + titleLabel.Location = new System.Drawing.Point(99, 46); + titleLabel.Name = "titleLabel"; + titleLabel.Size = new System.Drawing.Size(32, 17); + titleLabel.TabIndex = 14; + titleLabel.Text = "Title"; + // + // titleBox + // + titleBox.Location = new System.Drawing.Point(137, 43); + titleBox.Name = "titleBox"; + titleBox.Size = new System.Drawing.Size(365, 25); + titleBox.TabIndex = 15; + // // MainForm // AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; - ClientSize = new System.Drawing.Size(626, 416); + ClientSize = new System.Drawing.Size(937, 609); + Controls.Add(titleBox); + Controls.Add(titleLabel); Controls.Add(drawingNoBox); Controls.Add(equipmentBox); Controls.Add(mainTabControl); @@ -237,5 +257,7 @@ namespace ExportDXF.Forms private System.Windows.Forms.Label label1; private System.Windows.Forms.Label label2; private System.Windows.Forms.ComboBox drawingNoBox; + private System.Windows.Forms.Label titleLabel; + private System.Windows.Forms.TextBox titleBox; } } diff --git a/ExportDXF/Forms/MainForm.cs b/ExportDXF/Forms/MainForm.cs index f7d149b..cf124a3 100644 --- a/ExportDXF/Forms/MainForm.cs +++ b/ExportDXF/Forms/MainForm.cs @@ -1,4 +1,4 @@ -using ExportDXF.Data; +using ExportDXF.ApiClient; using ExportDXF.Extensions; using ExportDXF.Models; using ExportDXF.Services; @@ -18,15 +18,14 @@ namespace ExportDXF.Forms { private readonly ISolidWorksService _solidWorksService; private readonly IDxfExportService _exportService; - private readonly IFileExportService _fileExportService; - private readonly Func _dbContextFactory; + private readonly IFabWorksApiClient _apiClient; private CancellationTokenSource _cancellationTokenSource; private readonly BindingList _logEvents; private readonly BindingList _bomItems; private readonly BindingList _cutTemplates; private List _allDrawings; - public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFileExportService fileExportService, Func dbContextFactory = null) + public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFabWorksApiClient apiClient) { InitializeComponent(); _solidWorksService = solidWorksService ?? @@ -34,9 +33,8 @@ namespace ExportDXF.Forms _solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged; _exportService = exportService ?? throw new ArgumentNullException(nameof(exportService)); - _fileExportService = fileExportService ?? - throw new ArgumentNullException(nameof(fileExportService)); - _dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext()); + _apiClient = apiClient ?? + throw new ArgumentNullException(nameof(apiClient)); _logEvents = new BindingList(); _bomItems = new BindingList(); _cutTemplates = new BindingList(); @@ -70,9 +68,10 @@ namespace ExportDXF.Forms LogMessage("Connecting to SolidWorks, this may take a minute..."); await _solidWorksService.ConnectAsync(); _solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged; - LogMessage($"Output folder: {_fileExportService.OutputFolder}"); + LogMessage("Files will be uploaded to FabWorks API"); + await LoadDrawingDropdownsAsync(); LogMessage("Ready"); - UpdateActiveDocumentDisplay(); + await UpdateActiveDocumentDisplayAsync(); runButton.Enabled = true; } catch (Exception ex) @@ -274,74 +273,60 @@ namespace ExportDXF.Forms } private void InitializeDrawingDropdowns() + { + // Wire up event handler; actual data loading happens in LoadDrawingDropdownsAsync + equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged; + } + + private async Task LoadDrawingDropdownsAsync() { try { - using (var db = _dbContextFactory()) + var equipmentNumbers = await _apiClient.GetEquipmentNumbersAsync(); + + equipmentBox.Items.Clear(); + equipmentBox.Items.Add(""); + foreach (var eq in equipmentNumbers) { - // Get all drawing numbers from the database - var drawingNumbers = db.ExportRecords - .Select(r => r.DrawingNumber) - .Where(d => !string.IsNullOrEmpty(d)) - .Distinct() - .ToList(); - - // Parse into DrawingInfo objects - _allDrawings = drawingNumbers - .Select(DrawingInfo.Parse) - .Where(d => d != null) - .Distinct() - .OrderBy(d => d.EquipmentNo) - .ThenBy(d => d.DrawingNo) - .ToList(); - - // Get distinct equipment numbers - var equipmentNumbers = _allDrawings - .Select(d => d.EquipmentNo) - .Distinct() - .OrderBy(e => e) - .ToList(); - - // Populate equipment dropdown - equipmentBox.Items.Clear(); - equipmentBox.Items.Add(""); // Empty option for "all" - foreach (var eq in equipmentNumbers) - { - equipmentBox.Items.Add(eq); - } - - // Populate drawing dropdown with all drawings initially - UpdateDrawingDropdown(); - - // Wire up event handler for equipment selection change - equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged; + equipmentBox.Items.Add(eq); } + + // Clear _allDrawings — drawing list is now loaded on equipment selection + _allDrawings = new List(); + await UpdateDrawingDropdownAsync(); } catch (Exception ex) { - // Database might not exist yet - that's OK - System.Diagnostics.Debug.WriteLine($"Failed to load drawings from database: {ex.Message}"); + // API might not be available yet - that's OK + System.Diagnostics.Debug.WriteLine($"Failed to load equipment numbers from API: {ex.Message}"); } } - private void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e) + private async void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e) { - UpdateDrawingDropdown(); + await UpdateDrawingDropdownAsync(); } - private void UpdateDrawingDropdown() + private async Task UpdateDrawingDropdownAsync() { var selectedEquipment = equipmentBox.SelectedItem?.ToString(); - var filteredDrawings = string.IsNullOrEmpty(selectedEquipment) - ? _allDrawings - : _allDrawings.Where(d => d.EquipmentNo == selectedEquipment).ToList(); - drawingNoBox.Items.Clear(); - drawingNoBox.Items.Add(""); // Empty option - foreach (var drawing in filteredDrawings) + drawingNoBox.Items.Add(""); + + try { - drawingNoBox.Items.Add(drawing.DrawingNo); + var drawingNumbers = await _apiClient.GetDrawingNumbersByEquipmentAsync( + string.IsNullOrEmpty(selectedEquipment) ? null : selectedEquipment); + + foreach (var dn in drawingNumbers) + { + drawingNoBox.Items.Add(dn); + } + } + catch + { + // API might not be available } if (drawingNoBox.Items.Count > 0) @@ -380,16 +365,21 @@ namespace ExportDXF.Forms // Use equipment/drawing values from the UI dropdowns var equipment = equipmentBox.Text?.Trim(); var drawingNo = drawingNoBox.Text?.Trim(); - var filePrefix = !string.IsNullOrEmpty(equipment) && !string.IsNullOrEmpty(drawingNo) - ? $"{equipment} {drawingNo}" + var filePrefix = !string.IsNullOrEmpty(equipment) + ? (!string.IsNullOrEmpty(drawingNo) ? $"{equipment} {drawingNo}" : equipment) : activeDoc.Title; var viewFlipDecider = GetSelectedViewFlipDecider(); + var title = titleBox.Text?.Trim(); + var exportContext = new ExportContext { ActiveDocument = activeDoc, ViewFlipDecider = viewFlipDecider, FilePrefix = filePrefix, + Equipment = equipment, + DrawingNo = drawingNo, + Title = string.IsNullOrEmpty(title) ? null : title, EquipmentId = null, CancellationToken = token, ProgressCallback = (msg, level, file) => LogMessage(msg, level, file), @@ -401,11 +391,11 @@ namespace ExportDXF.Forms _cutTemplates.Clear(); LogMessage($"Started at {DateTime.Now:t}"); - LogMessage($"Exporting to: {_fileExportService.OutputFolder}"); + LogMessage("Exporting (files will be uploaded to API)..."); _solidWorksService.SetCommandInProgress(true); - await Task.Run(() => _exportService.Export(exportContext), token); + await Task.Run(async () => await _exportService.ExportAsync(exportContext), token); LogMessage("Done."); } @@ -452,32 +442,58 @@ namespace ExportDXF.Forms runButton.Enabled = true; } - private void OnActiveDocumentChanged(object sender, EventArgs e) + private async void OnActiveDocumentChanged(object sender, EventArgs e) { if (InvokeRequired) { - Invoke(new Action(() => OnActiveDocumentChanged(sender, e))); + Invoke(new Action(async () => await UpdateActiveDocumentDisplayAsync())); return; } - UpdateActiveDocumentDisplay(); + await UpdateActiveDocumentDisplayAsync(); } - private void UpdateActiveDocumentDisplay() + private async Task UpdateActiveDocumentDisplayAsync() { var activeDoc = _solidWorksService.GetActiveDocument(); var docTitle = activeDoc?.Title ?? "No Document Open"; this.Text = $"ExportDXF - {docTitle}"; - // Parse the file name and fill Equipment/Drawing dropdowns - if (activeDoc != null) + if (activeDoc == null) + return; + + // Try API first: look up the most recent export for this file path + DrawingInfo drawingInfo = null; + + if (!string.IsNullOrEmpty(activeDoc.FilePath)) { - var drawingInfo = DrawingInfo.Parse(activeDoc.Title); - if (drawingInfo != null) + drawingInfo = await LookupDrawingInfoFromHistoryAsync(activeDoc.FilePath); + } + + // Fall back to parsing the document title + if (drawingInfo == null) + { + drawingInfo = DrawingInfo.Parse(activeDoc.Title); + } + + if (drawingInfo != null) + { + // Detach event to prevent async race when setting equipment + equipmentBox.SelectedIndexChanged -= EquipmentBox_SelectedIndexChanged; + + if (!string.IsNullOrEmpty(drawingInfo.EquipmentNo)) { if (!equipmentBox.Items.Contains(drawingInfo.EquipmentNo)) equipmentBox.Items.Add(drawingInfo.EquipmentNo); equipmentBox.Text = drawingInfo.EquipmentNo; + } + // Load drawings for the selected equipment, then set drawing number + await UpdateDrawingDropdownAsync(); + + equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged; + + if (!string.IsNullOrEmpty(drawingInfo.DrawingNo)) + { if (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo)) drawingNoBox.Items.Add(drawingInfo.DrawingNo); drawingNoBox.Text = drawingInfo.DrawingNo; @@ -485,6 +501,26 @@ namespace ExportDXF.Forms } } + private async Task LookupDrawingInfoFromHistoryAsync(string filePath) + { + try + { + var dto = await _apiClient.GetExportBySourceFileAsync(filePath); + if (dto != null && !string.IsNullOrEmpty(dto.DrawingNumber)) + { + if (!string.IsNullOrEmpty(dto.Title)) + titleBox.Text = dto.Title; + return DrawingInfo.Parse(dto.DrawingNumber); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to look up drawing info from API: {ex.Message}"); + } + + return null; + } + private void LogMessage(string message, LogLevel level = LogLevel.Info, string file = null) { AddLogEvent(level, LogAction.Start, message, part: file); diff --git a/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.cs b/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.cs deleted file mode 100644 index bdb1d95..0000000 --- a/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ExportDXF.Migrations -{ - /// - public partial class ExtractCutTemplate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ContentHash", - table: "BomItems"); - - migrationBuilder.DropColumn( - name: "CutTemplateName", - table: "BomItems"); - - migrationBuilder.DropColumn( - name: "DefaultBendRadius", - table: "BomItems"); - - migrationBuilder.DropColumn( - name: "DxfFilePath", - table: "BomItems"); - - migrationBuilder.DropColumn( - name: "KFactor", - table: "BomItems"); - - migrationBuilder.DropColumn( - name: "Thickness", - table: "BomItems"); - - migrationBuilder.CreateTable( - name: "CutTemplates", - columns: table => new - { - Id = table.Column(type: "int", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - DxfFilePath = table.Column(type: "nvarchar(max)", nullable: true), - ContentHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), - CutTemplateName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), - Thickness = table.Column(type: "float", nullable: true), - KFactor = table.Column(type: "float", nullable: true), - DefaultBendRadius = table.Column(type: "float", nullable: true), - BomItemId = table.Column(type: "int", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_CutTemplates", x => x.Id); - table.ForeignKey( - name: "FK_CutTemplates_BomItems_BomItemId", - column: x => x.BomItemId, - principalTable: "BomItems", - principalColumn: "ID", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_CutTemplates_BomItemId", - table: "CutTemplates", - column: "BomItemId", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "CutTemplates"); - - migrationBuilder.AddColumn( - name: "ContentHash", - table: "BomItems", - type: "nvarchar(64)", - maxLength: 64, - nullable: true); - - migrationBuilder.AddColumn( - name: "CutTemplateName", - table: "BomItems", - type: "nvarchar(100)", - maxLength: 100, - nullable: true); - - migrationBuilder.AddColumn( - name: "DefaultBendRadius", - table: "BomItems", - type: "float", - nullable: true); - - migrationBuilder.AddColumn( - name: "DxfFilePath", - table: "BomItems", - type: "nvarchar(max)", - nullable: true); - - migrationBuilder.AddColumn( - name: "KFactor", - table: "BomItems", - type: "float", - nullable: true); - - migrationBuilder.AddColumn( - name: "Thickness", - table: "BomItems", - type: "float", - nullable: true); - } - } -} diff --git a/ExportDXF/Models/ExportContext.cs b/ExportDXF/Models/ExportContext.cs index 837db04..8a0ab2f 100644 --- a/ExportDXF/Models/ExportContext.cs +++ b/ExportDXF/Models/ExportContext.cs @@ -32,6 +32,21 @@ namespace ExportDXF.Services /// public string FilePrefix { get; set; } + /// + /// Equipment number from the UI (e.g., "5028"). + /// + public string Equipment { get; set; } + + /// + /// Drawing number from the UI (e.g., "A02", "Misc"). + /// + public string DrawingNo { get; set; } + + /// + /// Optional title/label for the export. + /// + public string Title { get; set; } + /// /// Selected Equipment ID for API operations (optional). /// diff --git a/ExportDXF/Models/ExportRecord.cs b/ExportDXF/Models/ExportRecord.cs index 77c201c..4e6adda 100644 --- a/ExportDXF/Models/ExportRecord.cs +++ b/ExportDXF/Models/ExportRecord.cs @@ -7,10 +7,13 @@ namespace ExportDXF.Models { public int Id { get; set; } public string DrawingNumber { get; set; } + public string EquipmentNo { get; set; } + public string DrawingNo { get; set; } public string SourceFilePath { get; set; } public string OutputFolder { get; set; } public DateTime ExportedAt { get; set; } public string ExportedBy { get; set; } + public string PdfContentHash { get; set; } public virtual ICollection BomItems { get; set; } = new List(); } diff --git a/ExportDXF/Models/Item.cs b/ExportDXF/Models/Item.cs index 6b92c68..629d537 100644 --- a/ExportDXF/Models/Item.cs +++ b/ExportDXF/Models/Item.cs @@ -61,5 +61,16 @@ namespace ExportDXF.Services /// The SolidWorks component reference. /// public Component2 Component { get; set; } + + /// + /// SHA256 content hash of the exported DXF (transient, not persisted). + /// + public string ContentHash { get; set; } + + /// + /// Full path to the locally-exported DXF temp file (transient, not persisted). + /// Set after successful export; used for upload to the API. + /// + public string LocalTempPath { get; set; } } } \ No newline at end of file diff --git a/ExportDXF/Program.cs b/ExportDXF/Program.cs index 4e68c80..05b5db5 100644 --- a/ExportDXF/Program.cs +++ b/ExportDXF/Program.cs @@ -1,7 +1,9 @@ +using ExportDXF.ApiClient; using ExportDXF.Forms; using ExportDXF.Services; using System; using System.Configuration; +using System.Net.Http; using System.Windows.Forms; namespace ExportDXF @@ -28,11 +30,11 @@ namespace ExportDXF /// public class ServiceContainer { - private readonly string _outputFolder; + private readonly string _apiBaseUrl; public ServiceContainer() { - _outputFolder = ConfigurationManager.AppSettings["ExportOutputFolder"] ?? @"C:\ExportDXF\Output"; + _apiBaseUrl = ConfigurationManager.AppSettings["FabWorksApiUrl"] ?? "http://localhost:5206"; } public MainForm ResolveMainForm() @@ -41,16 +43,22 @@ namespace ExportDXF var bomExtractor = new BomExtractor(); var partExporter = new PartExporter(); var drawingExporter = new DrawingExporter(); - var fileExportService = new FileExportService(_outputFolder); + + var httpClient = new HttpClient + { + BaseAddress = new Uri(_apiBaseUrl), + Timeout = TimeSpan.FromSeconds(30) + }; + var apiClient = new FabWorksApiClient(httpClient); var exportService = new DxfExportService( solidWorksService, bomExtractor, partExporter, drawingExporter, - fileExportService); + apiClient); - return new MainForm(solidWorksService, exportService, fileExportService); + return new MainForm(solidWorksService, exportService, apiClient); } } } diff --git a/ExportDXF/Services/DxfExportService.cs b/ExportDXF/Services/DxfExportService.cs index ff9bb42..5f8a67b 100644 --- a/ExportDXF/Services/DxfExportService.cs +++ b/ExportDXF/Services/DxfExportService.cs @@ -1,15 +1,15 @@ -using ExportDXF.Data; +using ExportDXF.ApiClient; using ExportDXF.Extensions; using ExportDXF.ItemExtractors; using ExportDXF.Models; using ExportDXF.Utilities; using ExportDXF; -using Microsoft.EntityFrameworkCore; using SolidWorks.Interop.sldworks; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace ExportDXF.Services { @@ -19,11 +19,12 @@ namespace ExportDXF.Services /// Exports the document specified in the context to DXF format. /// /// The export context containing all necessary information. - void Export(ExportContext context); + Task ExportAsync(ExportContext context); } /// /// Service responsible for orchestrating the export of SolidWorks documents to DXF format. + /// Files are generated locally in a temp directory, then uploaded to the API for storage and versioning. /// public class DxfExportService : IDxfExportService { @@ -31,29 +32,26 @@ namespace ExportDXF.Services private readonly IBomExtractor _bomExtractor; private readonly IPartExporter _partExporter; private readonly IDrawingExporter _drawingExporter; - private readonly IFileExportService _fileExportService; - private readonly Func _dbContextFactory; + private readonly IFabWorksApiClient _apiClient; public DxfExportService( ISolidWorksService solidWorksService, IBomExtractor bomExtractor, IPartExporter partExporter, IDrawingExporter drawingExporter, - IFileExportService fileExportService, - Func dbContextFactory = null) + IFabWorksApiClient apiClient) { _solidWorksService = solidWorksService ?? throw new ArgumentNullException(nameof(solidWorksService)); _bomExtractor = bomExtractor ?? throw new ArgumentNullException(nameof(bomExtractor)); _partExporter = partExporter ?? throw new ArgumentNullException(nameof(partExporter)); _drawingExporter = drawingExporter ?? throw new ArgumentNullException(nameof(drawingExporter)); - _fileExportService = fileExportService ?? throw new ArgumentNullException(nameof(fileExportService)); - _dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext()); + _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); } /// /// Exports the document specified in the context to DXF format. /// - public void Export(ExportContext context) + public async Task ExportAsync(ExportContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); @@ -62,23 +60,26 @@ namespace ExportDXF.Services SetupExportContext(context); var startTime = DateTime.Now; + var tempDir = CreateTempWorkDir(); try { _solidWorksService.EnableUserControl(false); + var drawingNumber = ParseDrawingNumber(context); + switch (context.ActiveDocument.DocumentType) { case DocumentType.Part: - ExportPart(context); + await ExportPartAsync(context, tempDir, drawingNumber); break; case DocumentType.Assembly: - ExportAssembly(context); + await ExportAssemblyAsync(context, tempDir, drawingNumber); break; case DocumentType.Drawing: - ExportDrawing(context); + await ExportDrawingAsync(context, drawingNumber, tempDir); break; default: @@ -90,6 +91,7 @@ namespace ExportDXF.Services { CleanupExportContext(context); _solidWorksService.EnableUserControl(true); + CleanupTempDir(tempDir); var duration = DateTime.Now - startTime; LogProgress(context, $"Run time: {duration.ToReadableFormat()}"); @@ -98,7 +100,7 @@ namespace ExportDXF.Services #region Export Methods by Document Type - private void ExportPart(ExportContext context) + private async Task ExportPartAsync(ExportContext context, string tempDir, string drawingNumber) { LogProgress(context, "Active document is a Part"); @@ -109,11 +111,54 @@ namespace ExportDXF.Services return; } - // Export directly to the output folder - _partExporter.ExportSinglePart(part, _fileExportService.OutputFolder, context); + var exportRecord = await CreateExportRecordAsync(context, drawingNumber); + var item = _partExporter.ExportSinglePart(part, tempDir, context); + + if (item != null) + { + // Check if this part+config already has a BOM item for this drawing + 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 ?? "" + }; + + // Upload DXF to API and get stored path + 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); + } } - private void ExportAssembly(ExportContext context) + private async Task ExportAssemblyAsync(ExportContext context, string tempDir, string drawingNumber) { LogProgress(context, "Active document is an Assembly"); LogProgress(context, "Fetching components..."); @@ -135,11 +180,31 @@ namespace ExportDXF.Services LogProgress(context, $"Found {items.Count} item(s)."); - // Export directly to the output folder - ExportItems(items, _fileExportService.OutputFolder, context); + var exportRecord = await CreateExportRecordAsync(context, drawingNumber); + + // Check existing BOM items and reuse item numbers, or assign new ones + var nextNum = int.Parse(await GetNextItemNumberAsync(drawingNumber)); + foreach (var item in items) + { + if (string.IsNullOrWhiteSpace(item.ItemNo)) + { + var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration); + if (existingItemNo != null) + { + item.ItemNo = existingItemNo; + } + else + { + item.ItemNo = nextNum.ToString(); + nextNum++; + } + } + } + + await ExportItemsAsync(items, tempDir, context, exportRecord?.Id); } - private void ExportDrawing(ExportContext context) + private async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir) { LogProgress(context, "Active document is a Drawing"); LogProgress(context, "Finding BOM tables..."); @@ -161,93 +226,46 @@ namespace ExportDXF.Services LogProgress(context, $"Found {items.Count} component(s)"); - // Determine drawing number for file naming - var drawingNumber = ParseDrawingNumber(context); - - // Resolve output folder: /{outputDir}/{equipmentNo}/{drawingNo}/ or flat fallback - var drawingOutputFolder = _fileExportService.GetDrawingOutputFolder(drawingNumber); - - // Export drawing to PDF - var tempDir = CreateTempWorkDir(); + // Export drawing to PDF in temp dir _drawingExporter.ExportToPdf(drawing, tempDir, context); - // Copy PDF to output folder with versioning - string pdfStashPath = null; - string savedPdfPath = null; + // Create export record via API + var exportRecord = await CreateExportRecordAsync(context, drawingNumber); + + // Upload PDF to API with versioning try { var pdfs = Directory.GetFiles(tempDir, "*.pdf"); if (pdfs.Length > 0) { - // Determine the destination path to stash the existing file - var pdfFileName = !string.IsNullOrEmpty(drawingNumber) - ? $"{drawingNumber}.pdf" - : Path.GetFileName(pdfs[0]); - var pdfDestPath = Path.Combine(drawingOutputFolder, pdfFileName); + var pdfTempPath = pdfs[0]; + var pdfHash = ContentHasher.ComputeFileHash(pdfTempPath); - pdfStashPath = _fileExportService.StashFile(pdfDestPath); - savedPdfPath = _fileExportService.SavePdfFile(pdfs[0], drawingNumber, drawingOutputFolder); - } - } - catch (Exception ex) - { - LogProgress(context, $"PDF save error: {ex.Message}", LogLevel.Error); - } + var uploadResult = await _apiClient.UploadPdfAsync( + pdfTempPath, + context.Equipment, + context.DrawingNo, + pdfHash, + exportRecord?.Id); - // Create export record in database - ExportRecord exportRecord = null; - try - { - using (var db = _dbContextFactory()) - { - db.Database.Migrate(); - exportRecord = new ExportRecord + if (uploadResult != null) { - DrawingNumber = drawingNumber ?? context.ActiveDocument.Title, - SourceFilePath = context.ActiveDocument.FilePath, - OutputFolder = drawingOutputFolder, - ExportedAt = DateTime.Now, - ExportedBy = System.Environment.UserName - }; - - // Handle PDF versioning - compute hash and compare with previous - if (savedPdfPath != null) - { - HandlePdfVersioning(savedPdfPath, exportRecord.DrawingNumber, exportRecord, context); - - // Archive or discard old PDF based on hash comparison - if (pdfStashPath != null) - { - var previousRecord = db.ExportRecords - .Where(r => r.DrawingNumber == exportRecord.DrawingNumber && r.PdfContentHash != null) - .OrderByDescending(r => r.Id) - .FirstOrDefault(); - - if (previousRecord != null && previousRecord.PdfContentHash == exportRecord.PdfContentHash) - { - _fileExportService.DiscardStash(pdfStashPath); - } - else - { - _fileExportService.ArchiveFile(pdfStashPath, savedPdfPath); - } - } + if (uploadResult.WasUnchanged) + LogProgress(context, $"PDF unchanged: {uploadResult.FileName}", LogLevel.Info); + else if (uploadResult.IsNewFile) + LogProgress(context, $"Saved PDF: {uploadResult.FileName}", LogLevel.Info); + else + LogProgress(context, $"PDF updated: {uploadResult.FileName}", LogLevel.Info); } - - db.ExportRecords.Add(exportRecord); - db.SaveChanges(); - LogProgress(context, $"Created export record (ID: {exportRecord.Id})", LogLevel.Info); } } catch (Exception ex) { - // Clean up stash on error - _fileExportService.DiscardStash(pdfStashPath); - LogProgress(context, $"Database error creating export record: {ex.Message}", LogLevel.Error); + LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error); } // Export parts to DXF and save BOM items - ExportItems(items, drawingOutputFolder, context, exportRecord?.Id); + await ExportItemsAsync(items, tempDir, context, exportRecord?.Id); } #endregion @@ -306,7 +324,7 @@ namespace ExportDXF.Services } } - private void ExportItems(List items, string saveDirectory, ExportContext context, int? exportRecordId = null) + private async Task ExportItemsAsync(List items, string tempDir, ExportContext context, int? exportRecordId = null) { int successCount = 0; int skippedCount = 0; @@ -324,7 +342,7 @@ namespace ExportDXF.Services try { // PartExporter will handle template drawing creation through context - _partExporter.ExportItem(item, saveDirectory, context); + _partExporter.ExportItem(item, tempDir, context); // Always create BomItem for every item (sheet metal or not) var bomItem = new BomItem @@ -341,23 +359,23 @@ namespace ExportDXF.Services Material = item.Material ?? "" }; - // Only create CutTemplate if DXF was exported successfully - if (!string.IsNullOrEmpty(item.FileName)) + // Only upload and create CutTemplate if DXF was exported successfully + if (!string.IsNullOrEmpty(item.LocalTempPath)) { successCount++; - var dxfPath = Path.Combine(saveDirectory, item.FileName + ".dxf"); - bomItem.CutTemplate = new CutTemplate + var uploadResult = await UploadDxfAsync(item, context); + if (uploadResult != null) { - DxfFilePath = dxfPath, - ContentHash = item.ContentHash, - Thickness = item.Thickness > 0 ? item.Thickness : null, - KFactor = item.KFactor > 0 ? item.KFactor : null, - DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null - }; - - // Compare hash with previous export to decide archive/discard - HandleDxfVersioning(item, dxfPath, context); + 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 + }; + } } else { @@ -367,21 +385,10 @@ namespace ExportDXF.Services // Add to UI context.BomItemCallback?.Invoke(bomItem); - // Save BOM item to database if we have an export record + // Save BOM item via API if we have an export record if (exportRecordId.HasValue) { - try - { - using (var db = _dbContextFactory()) - { - db.BomItems.Add(bomItem); - db.SaveChanges(); - } - } - catch (Exception dbEx) - { - LogProgress(context, $"Database error saving BOM item: {dbEx.Message}", LogLevel.Error); - } + await SaveBomItemAsync(exportRecordId.Value, bomItem, context); } } catch (Exception ex) @@ -398,86 +405,144 @@ namespace ExportDXF.Services if (exportRecordId.HasValue) { - LogProgress(context, $"BOM items saved to database (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info); + LogProgress(context, $"BOM items saved (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info); } } #endregion - #region Versioning + #region File Upload - private void HandleDxfVersioning(Item item, string dxfPath, ExportContext context) + private async Task UploadDxfAsync(Item item, ExportContext context) { - if (string.IsNullOrEmpty(item.ContentHash)) - return; - try { - using (var db = _dbContextFactory()) - { - var previousCutTemplate = db.CutTemplates - .Where(ct => ct.DxfFilePath == dxfPath && ct.ContentHash != null) - .OrderByDescending(ct => ct.Id) - .FirstOrDefault(); + var result = await _apiClient.UploadDxfAsync( + item.LocalTempPath, + context.Equipment, + context.DrawingNo, + item.ItemNo, + item.ContentHash); - if (previousCutTemplate != null && previousCutTemplate.ContentHash == item.ContentHash) - { - // Content unchanged - discard the stashed file - _fileExportService.DiscardStash(item.StashedFilePath); - LogProgress(context, $"DXF unchanged: {item.FileName}.dxf", LogLevel.Info); - } - else - { - // Content changed or first export - archive the old file - if (!string.IsNullOrEmpty(item.StashedFilePath)) - { - _fileExportService.ArchiveFile(item.StashedFilePath, dxfPath); - LogProgress(context, $"DXF updated, previous version archived: {item.FileName}.dxf", LogLevel.Info); - } - else - { - LogProgress(context, $"Exported: {item.FileName}.dxf", LogLevel.Info); - } - } - } + if (result.WasUnchanged) + LogProgress(context, $"DXF unchanged: {result.FileName}", LogLevel.Info); + else if (result.IsNewFile) + LogProgress(context, $"Exported: {result.FileName}", LogLevel.Info); + else + LogProgress(context, $"DXF updated: {result.FileName}", LogLevel.Info); + + return result; } catch (Exception ex) { - // Don't fail the export if versioning fails - just discard the stash - _fileExportService.DiscardStash(item.StashedFilePath); - LogProgress(context, $"Versioning check failed for {item.FileName}: {ex.Message}", LogLevel.Warning); + LogProgress(context, $"DXF upload failed for {item.FileName}: {ex.Message}", LogLevel.Warning); + return null; } } - private void HandlePdfVersioning(string pdfPath, string drawingNumber, ExportRecord exportRecord, ExportContext context) + #endregion + + #region API Helpers + + private async Task CreateExportRecordAsync(ExportContext context, string drawingNumber) { try { - var newHash = ContentHasher.ComputeFileHash(pdfPath); + var dto = await _apiClient.CreateExportAsync( + drawingNumber ?? context.ActiveDocument.Title, + context.Equipment ?? "", + context.DrawingNo ?? "", + context.ActiveDocument.FilePath, + "", // Output folder is now managed by the API + context.Title); - using (var db = _dbContextFactory()) + var record = new ExportRecord { - var previousRecord = db.ExportRecords - .Where(r => r.DrawingNumber == drawingNumber && r.PdfContentHash != null) - .OrderByDescending(r => r.Id) - .FirstOrDefault(); + Id = dto.Id, + DrawingNumber = dto.DrawingNumber, + EquipmentNo = dto.EquipmentNo, + DrawingNo = dto.DrawingNo, + SourceFilePath = dto.SourceFilePath, + OutputFolder = dto.OutputFolder, + ExportedAt = dto.ExportedAt, + ExportedBy = dto.ExportedBy + }; - if (previousRecord != null && previousRecord.PdfContentHash == newHash) - { - LogProgress(context, $"PDF unchanged: {Path.GetFileName(pdfPath)}", LogLevel.Info); - } - else - { - LogProgress(context, $"Saved PDF: {Path.GetFileName(pdfPath)}", LogLevel.Info); - } - } - - if (exportRecord != null) - exportRecord.PdfContentHash = newHash; + LogProgress(context, $"Created export record (ID: {record.Id})", LogLevel.Info); + return record; } catch (Exception ex) { - LogProgress(context, $"PDF versioning check failed: {ex.Message}", LogLevel.Warning); + LogProgress(context, $"API error creating export record: {ex.Message}", LogLevel.Error); + return null; + } + } + + private async Task FindExistingItemNoAsync(int? exportRecordId, string partName, string configurationName) + { + if (!exportRecordId.HasValue) + return null; + + try + { + var existing = await _apiClient.FindExistingBomItemAsync(exportRecordId.Value, partName, configurationName); + return existing?.ItemNo; + } + catch + { + return null; + } + } + + private async Task GetNextItemNumberAsync(string drawingNumber) + { + if (string.IsNullOrEmpty(drawingNumber)) + return "1"; + + try + { + return await _apiClient.GetNextItemNumberAsync(drawingNumber); + } + catch + { + return "1"; + } + } + + private async Task SaveBomItemAsync(int exportRecordId, BomItem bomItem, ExportContext context) + { + try + { + var apiBomItem = new ApiBomItem + { + ItemNo = bomItem.ItemNo, + PartNo = bomItem.PartNo, + SortOrder = bomItem.SortOrder, + Qty = bomItem.Qty, + TotalQty = bomItem.TotalQty, + Description = bomItem.Description, + PartName = bomItem.PartName, + ConfigurationName = bomItem.ConfigurationName, + Material = bomItem.Material + }; + + if (bomItem.CutTemplate != null) + { + apiBomItem.CutTemplate = new ApiCutTemplate + { + DxfFilePath = bomItem.CutTemplate.DxfFilePath, + ContentHash = bomItem.CutTemplate.ContentHash, + Thickness = bomItem.CutTemplate.Thickness, + KFactor = bomItem.CutTemplate.KFactor, + DefaultBendRadius = bomItem.CutTemplate.DefaultBendRadius + }; + } + + await _apiClient.CreateBomItemAsync(exportRecordId, apiBomItem); + } + catch (Exception ex) + { + LogProgress(context, $"API error saving BOM item: {ex.Message}", LogLevel.Error); } } @@ -492,9 +557,30 @@ namespace ExportDXF.Services return path; } + private void CleanupTempDir(string tempDir) + { + try + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, recursive: true); + } + catch + { + // Best-effort cleanup + } + } + private string ParseDrawingNumber(ExportContext context) { - // Prefer prefix (e.g., "5007 A02 PT"), fallback to active document title + // Use explicit Equipment/DrawingNo from the UI when available + if (!string.IsNullOrWhiteSpace(context?.Equipment)) + { + return !string.IsNullOrWhiteSpace(context?.DrawingNo) + ? $"{context.Equipment} {context.DrawingNo}" + : context.Equipment; + } + + // Fallback: parse from prefix or document title var candidate = context?.FilePrefix; var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate); if (info == null) @@ -502,7 +588,7 @@ namespace ExportDXF.Services var title = context?.ActiveDocument?.Title; info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title); } - return info != null ? ($"{info.EquipmentNo} {info.DrawingNo}") : null; + return info?.ToString(); } private void ValidateContext(ExportContext context) diff --git a/ExportDXF/Services/FileExportService.cs b/ExportDXF/Services/FileExportService.cs deleted file mode 100644 index 9e42f6d..0000000 --- a/ExportDXF/Services/FileExportService.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.IO; - -namespace ExportDXF.Services -{ - public interface IFileExportService - { - string OutputFolder { get; } - string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo); - string SavePdfFile(string sourcePath, string drawingNumber); - void EnsureOutputFolderExists(); - } - - public class FileExportService : IFileExportService - { - public string OutputFolder { get; } - - public FileExportService(string outputFolder) - { - OutputFolder = outputFolder ?? throw new ArgumentNullException(nameof(outputFolder)); - EnsureOutputFolderExists(); - } - - public void EnsureOutputFolderExists() - { - if (!Directory.Exists(OutputFolder)) - { - Directory.CreateDirectory(OutputFolder); - } - } - - public string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo) - { - if (string.IsNullOrEmpty(sourcePath)) - throw new ArgumentNullException(nameof(sourcePath)); - - var fileName = !string.IsNullOrEmpty(drawingNumber) && !string.IsNullOrEmpty(itemNo) - ? $"{drawingNumber} PT{itemNo}.dxf" - : Path.GetFileName(sourcePath); - - var destPath = Path.Combine(OutputFolder, fileName); - - // If source and dest are the same, skip copy - if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase)) - { - File.Copy(sourcePath, destPath, overwrite: true); - } - - return destPath; - } - - public string SavePdfFile(string sourcePath, string drawingNumber) - { - if (string.IsNullOrEmpty(sourcePath)) - throw new ArgumentNullException(nameof(sourcePath)); - - var fileName = !string.IsNullOrEmpty(drawingNumber) - ? $"{drawingNumber}.pdf" - : Path.GetFileName(sourcePath); - - var destPath = Path.Combine(OutputFolder, fileName); - - // If source and dest are the same, skip copy - if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase)) - { - File.Copy(sourcePath, destPath, overwrite: true); - } - - return destPath; - } - } -} diff --git a/ExportDXF/Services/PartExporter.cs b/ExportDXF/Services/PartExporter.cs index 405bdf5..789e9bf 100644 --- a/ExportDXF/Services/PartExporter.cs +++ b/ExportDXF/Services/PartExporter.cs @@ -1,4 +1,4 @@ -using ExportDXF.Extensions; +using ExportDXF.Extensions; using ExportDXF.Models; using ExportDXF.Utilities; using SolidWorks.Interop.sldworks; @@ -15,24 +15,29 @@ namespace ExportDXF.Services { /// /// Exports a single part document to DXF. + /// Returns an Item with export metadata (filename, hash, sheet metal properties), or null if export failed. /// /// The part document to export. - /// The directory where the DXF file will be saved. + /// The temp directory where the DXF file will be saved. /// The export context. - void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context); + Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context); /// /// Exports an item (component from BOM or assembly) to DXF. /// /// The item to export. - /// The directory where the DXF file will be saved. + /// The temp directory where the DXF file will be saved. /// The export context. void ExportItem(Item item, string saveDirectory, ExportContext context); } public class PartExporter : IPartExporter { - public void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context) + public PartExporter() + { + } + + public Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context) { if (part == null) throw new ArgumentNullException(nameof(part)); @@ -52,9 +57,49 @@ namespace ExportDXF.Services var fileName = GetSinglePartFileName(model, context.FilePrefix); var savePath = Path.Combine(saveDirectory, fileName + ".dxf"); + // Build result item with metadata + var item = new Item + { + PartName = model.GetTitle()?.Replace(".SLDPRT", "") ?? "", + Configuration = originalConfigName ?? "", + Quantity = 1 + }; + + // Enrich with sheet metal properties and description + var sheetMetalProps = SolidWorksHelper.GetSheetMetalProperties(model); + if (sheetMetalProps != null) + { + item.Thickness = sheetMetalProps.Thickness; + item.KFactor = sheetMetalProps.KFactor; + item.BendRadius = sheetMetalProps.BendRadius; + } + + // Get description from custom properties + var configPropMgr = model.Extension.CustomPropertyManager[originalConfigName]; + item.Description = configPropMgr?.Get("Description"); + if (string.IsNullOrEmpty(item.Description)) + { + var docPropMgr = model.Extension.CustomPropertyManager[""]; + item.Description = docPropMgr?.Get("Description"); + } + item.Description = TextHelper.RemoveXmlTags(item.Description); + + // Get material + item.Material = part.GetMaterialPropertyName2(originalConfigName, out _); + context.GetOrCreateTemplateDrawing(); - ExportPartToDxf(part, originalConfigName, savePath, context); + if (ExportPartToDxf(part, originalConfigName, savePath, context)) + { + item.FileName = Path.GetFileNameWithoutExtension(savePath); + item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath); + item.LocalTempPath = savePath; + return item; + } + else + { + return null; + } } finally { @@ -102,6 +147,8 @@ namespace ExportDXF.Services if (ExportPartToDxf(part, item.Component.ReferencedConfiguration, savePath, context)) { item.FileName = Path.GetFileNameWithoutExtension(savePath); + item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath); + item.LocalTempPath = savePath; } else { @@ -260,6 +307,7 @@ namespace ExportDXF.Services { var etcher = new EtchBendLines.Etcher(); etcher.AddEtchLines(dxfPath); + FixDegreeSymbol(dxfPath); } catch (Exception) { @@ -267,25 +315,38 @@ namespace ExportDXF.Services } } + /// + /// 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 + /// + 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 prefix) { var title = model.GetTitle().Replace(".SLDPRT", ""); var config = model.ConfigurationManager.ActiveConfiguration.Name; var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase); - var name = isDefaultConfig ? title : $"{title} [{config}]"; - return prefix + name; + return isDefaultConfig ? title : $"{title} [{config}]"; } private string GetItemFileName(Item item, string prefix) { - prefix = prefix?.Replace("\"", "''") ?? string.Empty; - if (string.IsNullOrWhiteSpace(item.ItemNo)) - { - return prefix + item.PartName; - } + return item.PartName; + prefix = prefix?.Replace("\"", "''") ?? string.Empty; var num = item.ItemNo.PadLeft(2, '0'); // Expected format: {DrawingNo} PT{ItemNo} return string.IsNullOrWhiteSpace(prefix) diff --git a/ExportDXF/Utilities/ContentHasher.cs b/ExportDXF/Utilities/ContentHasher.cs new file mode 100644 index 0000000..0be2612 --- /dev/null +++ b/ExportDXF/Utilities/ContentHasher.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace ExportDXF.Utilities +{ + public static class ContentHasher + { + /// + /// Computes a SHA256 hash of DXF file content, skipping the HEADER section + /// which contains timestamps that change on every save. + /// + 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()) + { + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content)); + return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); + } + } + + /// + /// Computes a SHA256 hash of the entire file contents (for PDFs and other binary files). + /// + public static string ComputeFileHash(string filePath) + { + using (var sha = SHA256.Create()) + using (var stream = File.OpenRead(filePath)) + { + var bytes = sha.ComputeHash(stream); + return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); + } + } + + /// + /// 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. + /// + private static int FindEndOfHeader(string text) + { + // Find the HEADER section start + var headerIndex = FindGroupCode(text, 0, "2", "HEADER"); + if (headerIndex < 0) + return -1; + + // Advance past the HEADER value line so pair scanning stays aligned + var headerLineEnd = text.IndexOf('\n', headerIndex); + if (headerLineEnd < 0) + return -1; + + // Find the ENDSEC that closes the HEADER section + var pos = headerLineEnd + 1; + while (pos < text.Length) + { + var endsecIndex = FindGroupCode(text, pos, "0", "ENDSEC"); + if (endsecIndex < 0) + return -1; + + // Move past the ENDSEC line + var lineEnd = text.IndexOf('\n', endsecIndex); + return lineEnd >= 0 ? lineEnd + 1 : text.Length; + } + + return -1; + } + + /// + /// 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. + /// + private static int FindGroupCode(string text, int startIndex, string groupCode, string value) + { + var pos = startIndex; + while (pos < text.Length) + { + // Skip whitespace/newlines to find the group code + while (pos < text.Length && (text[pos] == '\r' || text[pos] == '\n' || text[pos] == ' ')) + pos++; + + if (pos >= text.Length) + break; + + // Read the group code line + var codeLineEnd = text.IndexOf('\n', pos); + if (codeLineEnd < 0) + break; + + var codeLine = text.Substring(pos, codeLineEnd - pos).Trim(); + + // Move to the value line + var valueStart = codeLineEnd + 1; + if (valueStart >= text.Length) + break; + + var valueLineEnd = text.IndexOf('\n', valueStart); + if (valueLineEnd < 0) + valueLineEnd = text.Length; + + var valueLine = text.Substring(valueStart, valueLineEnd - valueStart).Trim(); + + if (codeLine == groupCode && string.Equals(valueLine, value, StringComparison.OrdinalIgnoreCase)) + return valueStart; + + // Move to the next pair + pos = valueLineEnd + 1; + } + + return -1; + } + } +} diff --git a/ExportDXF/app.config b/ExportDXF/app.config index f67a830..169406a 100644 --- a/ExportDXF/app.config +++ b/ExportDXF/app.config @@ -5,11 +5,6 @@ - + - - - diff --git a/FabWorks.Api/Configuration/FileStorageOptions.cs b/FabWorks.Api/Configuration/FileStorageOptions.cs new file mode 100644 index 0000000..8204f99 --- /dev/null +++ b/FabWorks.Api/Configuration/FileStorageOptions.cs @@ -0,0 +1,9 @@ +namespace FabWorks.Api.Configuration +{ + public class FileStorageOptions + { + public const string SectionName = "FileStorage"; + + public string OutputFolder { get; set; } = @"C:\ExportDXF\Output"; + } +} diff --git a/FabWorks.Api/Controllers/BomItemsController.cs b/FabWorks.Api/Controllers/BomItemsController.cs new file mode 100644 index 0000000..f6027d1 --- /dev/null +++ b/FabWorks.Api/Controllers/BomItemsController.cs @@ -0,0 +1,259 @@ +using FabWorks.Api.DTOs; +using FabWorks.Core.Data; +using FabWorks.Core.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace FabWorks.Api.Controllers +{ + [ApiController] + [Route("api/exports/{exportId}/bom-items")] + public class BomItemsController : ControllerBase + { + private readonly FabWorksDbContext _db; + + public BomItemsController(FabWorksDbContext db) => _db = db; + + [HttpGet("find")] + public async Task> FindExisting(int exportId, [FromQuery] string partName, [FromQuery] string configurationName) + { + var export = await _db.ExportRecords.FindAsync(exportId); + if (export == null) return NotFound(); + + var existing = await _db.BomItems + .Include(b => b.CutTemplate) + .Include(b => b.FormProgram) + .Include(b => b.ExportRecord) + .Where(b => b.ExportRecord.DrawingNumber == export.DrawingNumber + && b.PartName == (partName ?? "") + && b.ConfigurationName == (configurationName ?? "")) + .OrderByDescending(b => b.ID) + .FirstOrDefaultAsync(); + + if (existing == null) return NotFound(); + return MapToDto(existing); + } + + [HttpGet] + public async Task>> GetByExport(int exportId) + { + var items = await _db.BomItems + .Include(b => b.CutTemplate) + .Include(b => b.FormProgram) + .Where(b => b.ExportRecordId == exportId) + .OrderBy(b => b.SortOrder) + .ToListAsync(); + + return items.Select(MapToDto).ToList(); + } + + [HttpPost] + public async Task> Create(int exportId, BomItemDto dto) + { + var export = await _db.ExportRecords.FindAsync(exportId); + if (export == null) return NotFound("Export record not found"); + + // Look up the latest CutTemplate for this drawing+item across all previous exports + // to determine the revision number + var newContentHash = dto.CutTemplate?.ContentHash; + int revision = await ResolveRevisionAsync(export.DrawingNumber, dto.ItemNo, newContentHash); + + // Look for existing BomItem with same PartName + ConfigurationName within this export record + var existing = await _db.BomItems + .Include(b => b.CutTemplate) + .Include(b => b.FormProgram) + .Where(b => b.ExportRecordId == exportId + && b.PartName == (dto.PartName ?? "") + && b.ConfigurationName == (dto.ConfigurationName ?? "")) + .OrderByDescending(b => b.ID) + .FirstOrDefaultAsync(); + + if (existing != null) + { + // Update existing fields + existing.PartNo = dto.PartNo ?? ""; + existing.SortOrder = dto.SortOrder; + existing.Qty = dto.Qty; + existing.TotalQty = dto.TotalQty; + existing.Description = dto.Description ?? ""; + existing.Material = dto.Material ?? ""; + + if (dto.CutTemplate != null) + { + if (existing.CutTemplate != null) + { + existing.CutTemplate.DxfFilePath = dto.CutTemplate.DxfFilePath ?? ""; + existing.CutTemplate.ContentHash = dto.CutTemplate.ContentHash; + existing.CutTemplate.Revision = revision; + existing.CutTemplate.Thickness = dto.CutTemplate.Thickness; + existing.CutTemplate.KFactor = dto.CutTemplate.KFactor; + existing.CutTemplate.DefaultBendRadius = dto.CutTemplate.DefaultBendRadius; + } + else + { + existing.CutTemplate = new CutTemplate + { + DxfFilePath = dto.CutTemplate.DxfFilePath ?? "", + ContentHash = dto.CutTemplate.ContentHash, + Revision = revision, + Thickness = dto.CutTemplate.Thickness, + KFactor = dto.CutTemplate.KFactor, + DefaultBendRadius = dto.CutTemplate.DefaultBendRadius + }; + } + } + + if (dto.FormProgram != null) + { + if (existing.FormProgram != null) + { + existing.FormProgram.ProgramFilePath = dto.FormProgram.ProgramFilePath ?? ""; + existing.FormProgram.ContentHash = dto.FormProgram.ContentHash; + existing.FormProgram.ProgramName = dto.FormProgram.ProgramName ?? ""; + existing.FormProgram.Thickness = dto.FormProgram.Thickness; + existing.FormProgram.MaterialType = dto.FormProgram.MaterialType ?? ""; + existing.FormProgram.KFactor = dto.FormProgram.KFactor; + existing.FormProgram.BendCount = dto.FormProgram.BendCount; + existing.FormProgram.UpperToolNames = dto.FormProgram.UpperToolNames ?? ""; + existing.FormProgram.LowerToolNames = dto.FormProgram.LowerToolNames ?? ""; + existing.FormProgram.SetupNotes = dto.FormProgram.SetupNotes ?? ""; + } + else + { + existing.FormProgram = new FormProgram + { + ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "", + ContentHash = dto.FormProgram.ContentHash, + ProgramName = dto.FormProgram.ProgramName ?? "", + Thickness = dto.FormProgram.Thickness, + MaterialType = dto.FormProgram.MaterialType ?? "", + KFactor = dto.FormProgram.KFactor, + BendCount = dto.FormProgram.BendCount, + UpperToolNames = dto.FormProgram.UpperToolNames ?? "", + LowerToolNames = dto.FormProgram.LowerToolNames ?? "", + SetupNotes = dto.FormProgram.SetupNotes ?? "" + }; + } + } + + await _db.SaveChangesAsync(); + return Ok(MapToDto(existing)); + } + + // No existing match — create new + var item = new BomItem + { + ExportRecordId = exportId, + ItemNo = dto.ItemNo ?? "", + PartNo = dto.PartNo ?? "", + SortOrder = dto.SortOrder, + Qty = dto.Qty, + TotalQty = dto.TotalQty, + Description = dto.Description ?? "", + PartName = dto.PartName ?? "", + ConfigurationName = dto.ConfigurationName ?? "", + Material = dto.Material ?? "" + }; + + if (dto.CutTemplate != null) + { + item.CutTemplate = new CutTemplate + { + DxfFilePath = dto.CutTemplate.DxfFilePath ?? "", + ContentHash = dto.CutTemplate.ContentHash, + Revision = revision, + Thickness = dto.CutTemplate.Thickness, + KFactor = dto.CutTemplate.KFactor, + DefaultBendRadius = dto.CutTemplate.DefaultBendRadius + }; + } + + if (dto.FormProgram != null) + { + item.FormProgram = new FormProgram + { + ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "", + ContentHash = dto.FormProgram.ContentHash, + ProgramName = dto.FormProgram.ProgramName ?? "", + Thickness = dto.FormProgram.Thickness, + MaterialType = dto.FormProgram.MaterialType ?? "", + KFactor = dto.FormProgram.KFactor, + BendCount = dto.FormProgram.BendCount, + UpperToolNames = dto.FormProgram.UpperToolNames ?? "", + LowerToolNames = dto.FormProgram.LowerToolNames ?? "", + SetupNotes = dto.FormProgram.SetupNotes ?? "" + }; + } + + _db.BomItems.Add(item); + await _db.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetByExport), new { exportId }, MapToDto(item)); + } + + /// + /// Determines the revision number for a CutTemplate by looking at the most recent + /// CutTemplate for the same drawing number and item number across all exports. + /// Returns 1 if no previous version exists, the same revision if the hash matches, + /// or previous revision + 1 if the hash changed. + /// + private async Task ResolveRevisionAsync(string drawingNumber, string itemNo, string contentHash) + { + if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo) || string.IsNullOrEmpty(contentHash)) + return 1; + + var previous = await _db.CutTemplates + .Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber + && c.BomItem.ItemNo == itemNo + && c.ContentHash != null) + .OrderByDescending(c => c.Id) + .Select(c => new { c.ContentHash, c.Revision }) + .FirstOrDefaultAsync(); + + if (previous == null) + return 1; + + return previous.ContentHash == contentHash + ? previous.Revision + : previous.Revision + 1; + } + + private static BomItemDto MapToDto(BomItem b) => new() + { + ID = b.ID, + ItemNo = b.ItemNo, + PartNo = b.PartNo, + SortOrder = b.SortOrder, + Qty = b.Qty, + TotalQty = b.TotalQty, + Description = b.Description, + PartName = b.PartName, + ConfigurationName = b.ConfigurationName, + Material = b.Material, + CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto + { + Id = b.CutTemplate.Id, + DxfFilePath = b.CutTemplate.DxfFilePath, + ContentHash = b.CutTemplate.ContentHash, + Revision = b.CutTemplate.Revision, + Thickness = b.CutTemplate.Thickness, + KFactor = b.CutTemplate.KFactor, + DefaultBendRadius = b.CutTemplate.DefaultBendRadius + }, + FormProgram = b.FormProgram == null ? null : new FormProgramDto + { + Id = b.FormProgram.Id, + ProgramFilePath = b.FormProgram.ProgramFilePath, + ContentHash = b.FormProgram.ContentHash, + ProgramName = b.FormProgram.ProgramName, + Thickness = b.FormProgram.Thickness, + MaterialType = b.FormProgram.MaterialType, + KFactor = b.FormProgram.KFactor, + BendCount = b.FormProgram.BendCount, + UpperToolNames = b.FormProgram.UpperToolNames, + LowerToolNames = b.FormProgram.LowerToolNames, + SetupNotes = b.FormProgram.SetupNotes + } + }; + } +} diff --git a/FabWorks.Api/Controllers/ExportsController.cs b/FabWorks.Api/Controllers/ExportsController.cs new file mode 100644 index 0000000..81cb8ea --- /dev/null +++ b/FabWorks.Api/Controllers/ExportsController.cs @@ -0,0 +1,366 @@ +using System.IO.Compression; +using FabWorks.Api.DTOs; +using FabWorks.Api.Services; +using FabWorks.Core.Data; +using FabWorks.Core.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace FabWorks.Api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class ExportsController : ControllerBase + { + private readonly FabWorksDbContext _db; + private readonly IFileStorageService _fileStorage; + + public ExportsController(FabWorksDbContext db, IFileStorageService fileStorage) + { + _db = db; + _fileStorage = fileStorage; + } + + [HttpGet] + public async Task> List( + [FromQuery] string search = null, + [FromQuery] int skip = 0, + [FromQuery] int take = 50) + { + var query = _db.ExportRecords + .Include(r => r.BomItems) + .AsQueryable(); + + if (!string.IsNullOrWhiteSpace(search)) + { + var term = search.Trim().ToLower(); + query = query.Where(r => + r.DrawingNumber.ToLower().Contains(term) || + (r.Title != null && r.Title.ToLower().Contains(term)) || + r.ExportedBy.ToLower().Contains(term) || + r.BomItems.Any(b => b.PartName.ToLower().Contains(term) || + b.Description.ToLower().Contains(term))); + } + + var total = await query.CountAsync(); + + var records = await query + .OrderByDescending(r => r.ExportedAt) + .Skip(skip) + .Take(take) + .ToListAsync(); + + return new + { + total, + items = records.Select(r => new + { + r.Id, + r.DrawingNumber, + r.Title, + r.SourceFilePath, + r.ExportedAt, + r.ExportedBy, + BomItemCount = r.BomItems?.Count ?? 0 + }) + }; + } + + [HttpPost] + public async Task> Create(CreateExportRequest request) + { + var record = new ExportRecord + { + DrawingNumber = request.DrawingNumber, + Title = request.Title, + EquipmentNo = request.EquipmentNo, + DrawingNo = request.DrawingNo, + SourceFilePath = request.SourceFilePath, + OutputFolder = request.OutputFolder, + ExportedAt = DateTime.Now, + ExportedBy = Environment.UserName + }; + + _db.ExportRecords.Add(record); + await _db.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetById), new { id = record.Id }, MapToDto(record)); + } + + [HttpGet("{id}")] + public async Task> GetById(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(); + return MapToDto(record); + } + + [HttpGet("by-source")] + public async Task> GetBySourceFile([FromQuery] string path) + { + var record = await _db.ExportRecords + .Where(r => r.SourceFilePath.ToLower() == path.ToLower() + && !string.IsNullOrEmpty(r.DrawingNumber)) + .OrderByDescending(r => r.Id) + .FirstOrDefaultAsync(); + + if (record == null) return NotFound(); + return MapToDto(record); + } + + [HttpGet("by-drawing")] + public async Task>> GetByDrawing([FromQuery] string drawingNumber) + { + var records = await _db.ExportRecords + .Include(r => r.BomItems).ThenInclude(b => b.CutTemplate) + .Include(r => r.BomItems).ThenInclude(b => b.FormProgram) + .Where(r => r.DrawingNumber == drawingNumber) + .OrderByDescending(r => r.ExportedAt) + .ToListAsync(); + + return records.Select(MapToDto).ToList(); + } + + [HttpGet("next-item-number")] + public async Task> GetNextItemNumber([FromQuery] string drawingNumber) + { + if (string.IsNullOrEmpty(drawingNumber)) return "1"; + + var existingItems = await _db.ExportRecords + .Where(r => r.DrawingNumber == drawingNumber) + .SelectMany(r => r.BomItems) + .Select(b => b.ItemNo) + .ToListAsync(); + + int maxNum = 0; + foreach (var itemNo in existingItems) + { + if (int.TryParse(itemNo, out var num) && num > maxNum) + maxNum = num; + } + return (maxNum + 1).ToString(); + } + + [HttpGet("drawing-numbers")] + public async Task>> GetDrawingNumbers() + { + var numbers = await _db.ExportRecords + .Select(r => r.DrawingNumber) + .Where(d => !string.IsNullOrEmpty(d)) + .Distinct() + .ToListAsync(); + + return numbers; + } + + [HttpGet("equipment-numbers")] + public async Task>> GetEquipmentNumbers() + { + var numbers = await _db.ExportRecords + .Select(r => r.EquipmentNo) + .Where(e => !string.IsNullOrEmpty(e)) + .Distinct() + .OrderBy(e => e) + .ToListAsync(); + + return numbers; + } + + [HttpGet("drawing-numbers-by-equipment")] + public async Task>> GetDrawingNumbersByEquipment([FromQuery] string equipmentNo) + { + var query = _db.ExportRecords + .Where(r => !string.IsNullOrEmpty(r.DrawingNo)); + + if (!string.IsNullOrEmpty(equipmentNo)) + query = query.Where(r => r.EquipmentNo == equipmentNo); + + var numbers = await query + .Select(r => r.DrawingNo) + .Distinct() + .OrderBy(d => d) + .ToListAsync(); + + return numbers; + } + + [HttpGet("previous-pdf-hash")] + public async Task> GetPreviousPdfHash( + [FromQuery] string drawingNumber, + [FromQuery] int? excludeId = null) + { + var hash = await _db.ExportRecords + .Where(r => r.DrawingNumber == drawingNumber + && r.PdfContentHash != null + && (excludeId == null || r.Id != excludeId)) + .OrderByDescending(r => r.Id) + .Select(r => r.PdfContentHash) + .FirstOrDefaultAsync(); + + if (hash == null) return NotFound(); + return hash; + } + + [HttpPatch("{id}/pdf-hash")] + public async Task UpdatePdfHash(int id, [FromBody] UpdatePdfHashRequest request) + { + var record = await _db.ExportRecords.FindAsync(id); + if (record == null) return NotFound(); + + record.PdfContentHash = request.PdfContentHash; + await _db.SaveChangesAsync(); + + return NoContent(); + } + + [HttpGet("previous-cut-template")] + public async Task> GetPreviousCutTemplate( + [FromQuery] string drawingNumber, + [FromQuery] string itemNo) + { + if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo)) + return BadRequest("drawingNumber and itemNo are required."); + + var ct = await _db.CutTemplates + .Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber + && c.BomItem.ItemNo == itemNo + && c.ContentHash != null) + .OrderByDescending(c => c.Id) + .FirstOrDefaultAsync(); + + if (ct == null) return NotFound(); + + return new CutTemplateDto + { + Id = ct.Id, + DxfFilePath = ct.DxfFilePath, + ContentHash = ct.ContentHash, + Revision = ct.Revision, + Thickness = ct.Thickness, + KFactor = ct.KFactor, + DefaultBendRadius = ct.DefaultBendRadius + }; + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var record = await _db.ExportRecords + .Include(r => r.BomItems).ThenInclude(b => b.CutTemplate) + .Include(r => r.BomItems).ThenInclude(b => b.FormProgram) + .FirstOrDefaultAsync(r => r.Id == id); + + if (record == null) return NotFound(); + + _db.ExportRecords.Remove(record); + await _db.SaveChangesAsync(); + + return NoContent(); + } + + [HttpGet("{id}/download-dxfs")] + public async Task DownloadAllDxfs(int id) + { + var record = await _db.ExportRecords + .Include(r => r.BomItems).ThenInclude(b => b.CutTemplate) + .FirstOrDefaultAsync(r => r.Id == id); + + if (record == null) return NotFound(); + + var dxfItems = record.BomItems + .Where(b => b.CutTemplate?.ContentHash != null) + .ToList(); + + if (dxfItems.Count == 0) return NotFound("No DXF files for this export."); + + var ms = new MemoryStream(); + using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + var usedNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var b in dxfItems) + { + var ct = b.CutTemplate; + var fileName = ct.DxfFilePath?.Split(new[] { '/', '\\' }).LastOrDefault() + ?? $"PT{(b.ItemNo ?? "").PadLeft(2, '0')}.dxf"; + + // Ensure unique names in zip + if (!usedNames.Add(fileName)) + { + var baseName = Path.GetFileNameWithoutExtension(fileName); + var ext = Path.GetExtension(fileName); + var counter = 2; + do { fileName = $"{baseName}_{counter++}{ext}"; } + while (!usedNames.Add(fileName)); + } + + var blobStream = _fileStorage.OpenBlob(ct.ContentHash, "dxf"); + if (blobStream == null) continue; + + var entry = zip.CreateEntry(fileName, CompressionLevel.Fastest); + using var entryStream = entry.Open(); + await blobStream.CopyToAsync(entryStream); + blobStream.Dispose(); + } + } + + ms.Position = 0; + var zipName = $"{record.DrawingNumber ?? $"Export-{id}"} DXFs.zip"; + return File(ms, "application/zip", zipName); + } + + private static ExportDetailDto MapToDto(ExportRecord r) => new() + { + Id = r.Id, + DrawingNumber = r.DrawingNumber, + Title = r.Title, + EquipmentNo = r.EquipmentNo, + DrawingNo = r.DrawingNo, + SourceFilePath = r.SourceFilePath, + OutputFolder = r.OutputFolder, + ExportedAt = r.ExportedAt, + ExportedBy = r.ExportedBy, + PdfContentHash = r.PdfContentHash, + BomItems = r.BomItems?.Select(b => new BomItemDto + { + ID = b.ID, + ItemNo = b.ItemNo, + PartNo = b.PartNo, + SortOrder = b.SortOrder, + Qty = b.Qty, + TotalQty = b.TotalQty, + Description = b.Description, + PartName = b.PartName, + ConfigurationName = b.ConfigurationName, + Material = b.Material, + CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto + { + Id = b.CutTemplate.Id, + DxfFilePath = b.CutTemplate.DxfFilePath, + ContentHash = b.CutTemplate.ContentHash, + Revision = b.CutTemplate.Revision, + Thickness = b.CutTemplate.Thickness, + KFactor = b.CutTemplate.KFactor, + DefaultBendRadius = b.CutTemplate.DefaultBendRadius + }, + FormProgram = b.FormProgram == null ? null : new FormProgramDto + { + Id = b.FormProgram.Id, + ProgramFilePath = b.FormProgram.ProgramFilePath, + ContentHash = b.FormProgram.ContentHash, + ProgramName = b.FormProgram.ProgramName, + Thickness = b.FormProgram.Thickness, + MaterialType = b.FormProgram.MaterialType, + KFactor = b.FormProgram.KFactor, + BendCount = b.FormProgram.BendCount, + UpperToolNames = b.FormProgram.UpperToolNames, + LowerToolNames = b.FormProgram.LowerToolNames, + SetupNotes = b.FormProgram.SetupNotes + } + }).ToList() ?? new() + }; + } +} diff --git a/FabWorks.Api/Controllers/FileBrowserController.cs b/FabWorks.Api/Controllers/FileBrowserController.cs new file mode 100644 index 0000000..895b20f --- /dev/null +++ b/FabWorks.Api/Controllers/FileBrowserController.cs @@ -0,0 +1,184 @@ +using FabWorks.Api.Services; +using FabWorks.Core.Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.EntityFrameworkCore; + +namespace FabWorks.Api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class FileBrowserController : ControllerBase + { + private readonly IFileStorageService _fileStorage; + private readonly FabWorksDbContext _db; + private readonly FileExtensionContentTypeProvider _contentTypeProvider = new(); + + public FileBrowserController(IFileStorageService fileStorage, FabWorksDbContext db) + { + _fileStorage = fileStorage; + _db = db; + } + + [HttpGet("files")] + public async Task> ListFiles( + [FromQuery] string search = null, + [FromQuery] string type = null) + { + var files = new List(); + + // Query DXF files from CutTemplates + if (type == null || type.Equals("dxf", StringComparison.OrdinalIgnoreCase)) + { + var dxfQuery = _db.CutTemplates + .Where(c => c.ContentHash != null) + .Select(c => new + { + c.Id, + c.DxfFilePath, + c.ContentHash, + c.Thickness, + DrawingNumber = c.BomItem.ExportRecord.DrawingNumber, + CreatedAt = c.BomItem.ExportRecord.ExportedAt + }); + + if (!string.IsNullOrWhiteSpace(search)) + { + var term = search.Trim().ToLower(); + dxfQuery = dxfQuery.Where(c => + c.DxfFilePath.ToLower().Contains(term) || + c.DrawingNumber.ToLower().Contains(term)); + } + + var dxfResults = await dxfQuery + .OrderByDescending(c => c.CreatedAt) + .Take(500) + .ToListAsync(); + + // Deduplicate by content hash (keep latest) + var seenDxf = new HashSet(); + foreach (var c in dxfResults) + { + if (seenDxf.Contains(c.ContentHash)) continue; + seenDxf.Add(c.ContentHash); + + var fileName = c.DxfFilePath?.Split(new[] { '/', '\\' }).LastOrDefault() ?? c.DxfFilePath; + files.Add(new StoredFileEntry + { + FileName = fileName, + ContentHash = c.ContentHash, + FileType = "dxf", + DrawingNumber = c.DrawingNumber, + Thickness = c.Thickness, + CreatedAt = c.CreatedAt + }); + } + } + + // Query PDF files from ExportRecords + if (type == null || type.Equals("pdf", StringComparison.OrdinalIgnoreCase)) + { + var pdfQuery = _db.ExportRecords + .Where(r => r.PdfContentHash != null) + .Select(r => new + { + r.Id, + r.DrawingNumber, + r.PdfContentHash, + r.ExportedAt + }); + + if (!string.IsNullOrWhiteSpace(search)) + { + var term = search.Trim().ToLower(); + pdfQuery = pdfQuery.Where(r => + r.DrawingNumber.ToLower().Contains(term)); + } + + var pdfResults = await pdfQuery + .OrderByDescending(r => r.ExportedAt) + .Take(500) + .ToListAsync(); + + // Deduplicate by content hash + var seenPdf = new HashSet(); + foreach (var r in pdfResults) + { + if (seenPdf.Contains(r.PdfContentHash)) continue; + seenPdf.Add(r.PdfContentHash); + + files.Add(new StoredFileEntry + { + FileName = $"{r.DrawingNumber}.pdf", + ContentHash = r.PdfContentHash, + FileType = "pdf", + DrawingNumber = r.DrawingNumber, + CreatedAt = r.ExportedAt + }); + } + } + + return new FileListResult + { + Total = files.Count, + Files = files.OrderByDescending(f => f.CreatedAt).ToList() + }; + } + + [HttpGet("preview")] + public IActionResult PreviewFile([FromQuery] string hash, [FromQuery] string ext = "dxf") + { + if (string.IsNullOrEmpty(hash) || hash.Length < 4) + return BadRequest("Invalid hash."); + + if (!_fileStorage.BlobExists(hash, ext)) + return NotFound("File not found."); + + var stream = _fileStorage.OpenBlob(hash, ext); + if (stream == null) + return NotFound("File not found."); + + var virtualName = $"file.{ext}"; + if (!_contentTypeProvider.TryGetContentType(virtualName, out var contentType)) + contentType = "application/octet-stream"; + + return File(stream, contentType); + } + + [HttpGet("download")] + public IActionResult DownloadFile([FromQuery] string hash, [FromQuery] string ext = "dxf", [FromQuery] string name = null) + { + if (string.IsNullOrEmpty(hash) || hash.Length < 4) + return BadRequest("Invalid hash."); + + if (!_fileStorage.BlobExists(hash, ext)) + return NotFound("File not found."); + + var stream = _fileStorage.OpenBlob(hash, ext); + if (stream == null) + return NotFound("File not found."); + + var fileName = name ?? $"{hash[..8]}.{ext}"; + if (!_contentTypeProvider.TryGetContentType(fileName, out var contentType)) + contentType = "application/octet-stream"; + + return File(stream, contentType, fileName); + } + } + + public class FileListResult + { + public int Total { get; set; } + public List Files { get; set; } + } + + public class StoredFileEntry + { + public string FileName { get; set; } + public string ContentHash { get; set; } + public string FileType { get; set; } + public string DrawingNumber { get; set; } + public double? Thickness { get; set; } + public DateTime CreatedAt { get; set; } + } +} diff --git a/FabWorks.Api/Controllers/FilesController.cs b/FabWorks.Api/Controllers/FilesController.cs new file mode 100644 index 0000000..b8263f1 --- /dev/null +++ b/FabWorks.Api/Controllers/FilesController.cs @@ -0,0 +1,93 @@ +using FabWorks.Api.DTOs; +using FabWorks.Api.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; + +namespace FabWorks.Api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class FilesController : ControllerBase + { + private readonly IFileStorageService _fileStorage; + private readonly FileExtensionContentTypeProvider _contentTypeProvider = new(); + + public FilesController(IFileStorageService fileStorage) + { + _fileStorage = fileStorage; + } + + [HttpPost("dxf")] + [RequestSizeLimit(50_000_000)] // 50 MB + public async Task> UploadDxf( + IFormFile file, + [FromForm] string equipment, + [FromForm] string drawingNo, + [FromForm] string itemNo, + [FromForm] string contentHash) + { + if (file == null || file.Length == 0) + return BadRequest("No file uploaded."); + + using var stream = file.OpenReadStream(); + var result = await _fileStorage.StoreDxfAsync(stream, equipment, drawingNo, itemNo, contentHash); + + return Ok(new FileUploadResponse + { + StoredFilePath = result.FileName, + ContentHash = result.ContentHash, + FileName = result.FileName, + WasUnchanged = result.WasUnchanged, + IsNewFile = result.IsNewFile + }); + } + + [HttpPost("pdf")] + [RequestSizeLimit(100_000_000)] // 100 MB + public async Task> UploadPdf( + IFormFile file, + [FromForm] string equipment, + [FromForm] string drawingNo, + [FromForm] string contentHash, + [FromForm] int? exportRecordId = null) + { + if (file == null || file.Length == 0) + return BadRequest("No file uploaded."); + + using var stream = file.OpenReadStream(); + var result = await _fileStorage.StorePdfAsync(stream, equipment, drawingNo, contentHash, exportRecordId); + + return Ok(new FileUploadResponse + { + StoredFilePath = result.FileName, + ContentHash = result.ContentHash, + FileName = result.FileName, + WasUnchanged = result.WasUnchanged, + IsNewFile = result.IsNewFile + }); + } + + [HttpGet("blob/{hash}")] + public IActionResult GetBlob(string hash, [FromQuery] string ext = "dxf", [FromQuery] bool download = false, [FromQuery] string name = null) + { + if (string.IsNullOrEmpty(hash) || hash.Length < 4) + return BadRequest("Invalid hash."); + + if (!_fileStorage.BlobExists(hash, ext)) + return NotFound("Blob not found."); + + var stream = _fileStorage.OpenBlob(hash, ext); + if (stream == null) + return NotFound("Blob not found."); + + var fileName = !string.IsNullOrEmpty(name) ? name : $"{hash[..8]}.{ext}"; + if (!_contentTypeProvider.TryGetContentType(fileName, out var contentType)) + contentType = "application/octet-stream"; + + if (download) + return File(stream, contentType, fileName); + + return File(stream, contentType); + } + } +} diff --git a/FabWorks.Api/Controllers/FormProgramsController.cs b/FabWorks.Api/Controllers/FormProgramsController.cs new file mode 100644 index 0000000..39ab191 --- /dev/null +++ b/FabWorks.Api/Controllers/FormProgramsController.cs @@ -0,0 +1,106 @@ +using FabWorks.Api.DTOs; +using FabWorks.Api.Services; +using FabWorks.Core.Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace FabWorks.Api.Controllers +{ + [ApiController] + [Route("api/form-programs")] + public class FormProgramsController : ControllerBase + { + private readonly FabWorksDbContext _db; + private readonly FormProgramService _formService; + + public FormProgramsController(FabWorksDbContext db, FormProgramService formService) + { + _db = db; + _formService = formService; + } + + [HttpGet("by-drawing")] + public async Task>> GetByDrawing([FromQuery] string drawingNumber) + { + var programs = await _db.FormPrograms + .Include(fp => fp.BomItem) + .ThenInclude(b => b.ExportRecord) + .Where(fp => fp.BomItem.ExportRecord.DrawingNumber == drawingNumber) + .ToListAsync(); + + return programs.Select(fp => new FormProgramDto + { + Id = fp.Id, + ProgramFilePath = fp.ProgramFilePath, + ContentHash = fp.ContentHash, + ProgramName = fp.ProgramName, + Thickness = fp.Thickness, + MaterialType = fp.MaterialType, + KFactor = fp.KFactor, + BendCount = fp.BendCount, + UpperToolNames = fp.UpperToolNames, + LowerToolNames = fp.LowerToolNames, + SetupNotes = fp.SetupNotes + }).ToList(); + } + + [HttpPost("parse")] + public ActionResult Parse([FromQuery] string filePath) + { + if (!System.IO.File.Exists(filePath)) + return NotFound($"File not found: {filePath}"); + + var fp = _formService.ParseFromFile(filePath); + return new FormProgramDto + { + ProgramFilePath = fp.ProgramFilePath, + ContentHash = fp.ContentHash, + ProgramName = fp.ProgramName, + Thickness = fp.Thickness, + MaterialType = fp.MaterialType, + KFactor = fp.KFactor, + BendCount = fp.BendCount, + UpperToolNames = fp.UpperToolNames, + LowerToolNames = fp.LowerToolNames, + SetupNotes = fp.SetupNotes + }; + } + + [HttpPost("{bomItemId}")] + public async Task> AttachToItem(int bomItemId, [FromQuery] string filePath) + { + var bomItem = await _db.BomItems + .Include(b => b.FormProgram) + .FirstOrDefaultAsync(b => b.ID == bomItemId); + + if (bomItem == null) return NotFound("BOM item not found"); + + if (!System.IO.File.Exists(filePath)) + return NotFound($"File not found: {filePath}"); + + var fp = _formService.ParseFromFile(filePath); + fp.BomItemId = bomItemId; + + if (bomItem.FormProgram != null) + _db.FormPrograms.Remove(bomItem.FormProgram); + + bomItem.FormProgram = fp; + await _db.SaveChangesAsync(); + + return new FormProgramDto + { + Id = fp.Id, + ProgramFilePath = fp.ProgramFilePath, + ContentHash = fp.ContentHash, + ProgramName = fp.ProgramName, + Thickness = fp.Thickness, + MaterialType = fp.MaterialType, + KFactor = fp.KFactor, + BendCount = fp.BendCount, + UpperToolNames = fp.UpperToolNames, + LowerToolNames = fp.LowerToolNames, + SetupNotes = fp.SetupNotes + }; + } + } +} diff --git a/FabWorks.Api/DTOs/CreateExportRequest.cs b/FabWorks.Api/DTOs/CreateExportRequest.cs new file mode 100644 index 0000000..db9e94d --- /dev/null +++ b/FabWorks.Api/DTOs/CreateExportRequest.cs @@ -0,0 +1,17 @@ +namespace FabWorks.Api.DTOs +{ + public class CreateExportRequest + { + public string DrawingNumber { get; set; } + public string Title { get; set; } + public string EquipmentNo { get; set; } + public string DrawingNo { get; set; } + public string SourceFilePath { get; set; } + public string OutputFolder { get; set; } + } + + public class UpdatePdfHashRequest + { + public string PdfContentHash { get; set; } + } +} diff --git a/FabWorks.Api/DTOs/ExportDetailDto.cs b/FabWorks.Api/DTOs/ExportDetailDto.cs new file mode 100644 index 0000000..ed6eb78 --- /dev/null +++ b/FabWorks.Api/DTOs/ExportDetailDto.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace FabWorks.Api.DTOs +{ + public class ExportDetailDto + { + public int Id { get; set; } + public string DrawingNumber { get; set; } + public string Title { get; set; } + public string EquipmentNo { get; set; } + public string DrawingNo { get; set; } + public string SourceFilePath { get; set; } + public string OutputFolder { get; set; } + public DateTime ExportedAt { get; set; } + public string ExportedBy { get; set; } + public string PdfContentHash { get; set; } + public List BomItems { get; set; } = new(); + } + + public class BomItemDto + { + public int ID { get; set; } + public string ItemNo { get; set; } + public string PartNo { get; set; } + public int SortOrder { get; set; } + public int? Qty { get; set; } + public int? TotalQty { get; set; } + public string Description { get; set; } + public string PartName { get; set; } + public string ConfigurationName { get; set; } + public string Material { get; set; } + public CutTemplateDto CutTemplate { get; set; } + public FormProgramDto FormProgram { get; set; } + } + + public class CutTemplateDto + { + public int Id { get; set; } + public string DxfFilePath { get; set; } + public string ContentHash { get; set; } + public int Revision { get; set; } + public double? Thickness { get; set; } + public double? KFactor { get; set; } + public double? DefaultBendRadius { get; set; } + } + + public class FormProgramDto + { + public int Id { get; set; } + public string ProgramFilePath { get; set; } + public string ContentHash { get; set; } + public string ProgramName { get; set; } + public double? Thickness { get; set; } + public string MaterialType { get; set; } + public double? KFactor { get; set; } + public int BendCount { get; set; } + public string UpperToolNames { get; set; } + public string LowerToolNames { get; set; } + public string SetupNotes { get; set; } + } +} diff --git a/FabWorks.Api/DTOs/FileUploadResponse.cs b/FabWorks.Api/DTOs/FileUploadResponse.cs new file mode 100644 index 0000000..7a1fad0 --- /dev/null +++ b/FabWorks.Api/DTOs/FileUploadResponse.cs @@ -0,0 +1,11 @@ +namespace FabWorks.Api.DTOs +{ + public class FileUploadResponse + { + public string StoredFilePath { get; set; } // kept for client compat, contains logical filename + public string ContentHash { get; set; } + public string FileName { get; set; } + public bool WasUnchanged { get; set; } + public bool IsNewFile { get; set; } + } +} diff --git a/FabWorks.Api/FabWorks.Api.csproj b/FabWorks.Api/FabWorks.Api.csproj new file mode 100644 index 0000000..921afe9 --- /dev/null +++ b/FabWorks.Api/FabWorks.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + disable + enable + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/FabWorks.Api/Program.cs b/FabWorks.Api/Program.cs new file mode 100644 index 0000000..42e6c76 --- /dev/null +++ b/FabWorks.Api/Program.cs @@ -0,0 +1,26 @@ +using FabWorks.Api.Configuration; +using FabWorks.Api.Services; +using FabWorks.Core.Data; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb"))); +builder.Services.AddSingleton(); + +builder.Services.Configure( + builder.Configuration.GetSection(FileStorageOptions.SectionName)); +builder.Services.AddScoped(); + +var app = builder.Build(); + +app.UseDefaultFiles(); +app.UseStaticFiles(new StaticFileOptions +{ + OnPrepareResponse = ctx => + ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store") +}); +app.MapControllers(); +app.Run(); diff --git a/FabWorks.Api/Properties/launchSettings.json b/FabWorks.Api/Properties/launchSettings.json new file mode 100644 index 0000000..65e9483 --- /dev/null +++ b/FabWorks.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:45483", + "sslPort": 44397 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "http://localhost:5206", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:7182;http://localhost:5206", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/FabWorks.Api/Services/FileStorageService.cs b/FabWorks.Api/Services/FileStorageService.cs new file mode 100644 index 0000000..52cdb0b --- /dev/null +++ b/FabWorks.Api/Services/FileStorageService.cs @@ -0,0 +1,159 @@ +using FabWorks.Api.Configuration; +using FabWorks.Core.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace FabWorks.Api.Services +{ + public class FileUploadResult + { + public string ContentHash { get; set; } + public string FileName { get; set; } + public bool WasUnchanged { get; set; } + public bool IsNewFile { get; set; } + } + + public interface IFileStorageService + { + string OutputFolder { get; } + Task StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash); + Task StorePdfAsync(Stream stream, string equipment, string drawingNo, string contentHash, int? exportRecordId = null); + Stream OpenBlob(string contentHash, string extension); + bool BlobExists(string contentHash, string extension); + } + + public class FileStorageService : IFileStorageService + { + private readonly FileStorageOptions _options; + private readonly FabWorksDbContext _db; + + public string OutputFolder => _options.OutputFolder; + + public FileStorageService(IOptions options, FabWorksDbContext db) + { + _options = options.Value; + _db = db; + + var blobRoot = Path.Combine(_options.OutputFolder, "blobs"); + if (!Directory.Exists(blobRoot)) + Directory.CreateDirectory(blobRoot); + } + + public async Task StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash) + { + var fileName = BuildDxfFileName(drawingNo, equipment, itemNo); + + // Look up previous hash by drawing number + item number + var drawingNumber = BuildDrawingNumber(equipment, drawingNo); + var previousHash = await _db.CutTemplates + .Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber + && c.BomItem.ItemNo == itemNo + && c.ContentHash != null) + .OrderByDescending(c => c.Id) + .Select(c => c.ContentHash) + .FirstOrDefaultAsync(); + + var wasUnchanged = previousHash != null && previousHash == contentHash; + var isNewFile = await StoreBlobAsync(stream, contentHash, "dxf"); + + return new FileUploadResult + { + ContentHash = contentHash, + FileName = fileName, + WasUnchanged = wasUnchanged, + IsNewFile = isNewFile + }; + } + + public async Task StorePdfAsync(Stream stream, string equipment, string drawingNo, string contentHash, int? exportRecordId = null) + { + var drawingNumber = BuildDrawingNumber(equipment, drawingNo); + var fileName = $"{drawingNumber}.pdf"; + + // Look up previous PDF hash + var previousHash = await _db.ExportRecords + .Where(r => r.DrawingNumber == drawingNumber + && r.PdfContentHash != null + && (exportRecordId == null || r.Id != exportRecordId)) + .OrderByDescending(r => r.Id) + .Select(r => r.PdfContentHash) + .FirstOrDefaultAsync(); + + var wasUnchanged = previousHash != null && previousHash == contentHash; + var isNewFile = await StoreBlobAsync(stream, contentHash, "pdf"); + + // Update the export record with the PDF content hash + if (exportRecordId.HasValue) + { + var record = await _db.ExportRecords.FindAsync(exportRecordId.Value); + if (record != null) + { + record.PdfContentHash = contentHash; + await _db.SaveChangesAsync(); + } + } + + return new FileUploadResult + { + ContentHash = contentHash, + FileName = fileName, + WasUnchanged = wasUnchanged, + IsNewFile = isNewFile + }; + } + + public Stream OpenBlob(string contentHash, string extension) + { + var path = GetBlobPath(contentHash, extension); + if (!File.Exists(path)) + return null; + return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + public bool BlobExists(string contentHash, string extension) + { + return File.Exists(GetBlobPath(contentHash, extension)); + } + + private async Task StoreBlobAsync(Stream stream, string contentHash, string extension) + { + var blobPath = GetBlobPath(contentHash, extension); + + if (File.Exists(blobPath)) + return false; // blob already exists (dedup) + + var dir = Path.GetDirectoryName(blobPath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + using var fileStream = new FileStream(blobPath, FileMode.Create, FileAccess.Write); + await stream.CopyToAsync(fileStream); + return true; // new blob written + } + + private string GetBlobPath(string contentHash, string extension) + { + var prefix1 = contentHash[..2]; + var prefix2 = contentHash[2..4]; + return Path.Combine(_options.OutputFolder, "blobs", prefix1, prefix2, $"{contentHash}.{extension}"); + } + + private static string BuildDrawingNumber(string equipment, string drawingNo) + { + if (!string.IsNullOrEmpty(equipment) && !string.IsNullOrEmpty(drawingNo)) + return $"{equipment} {drawingNo}"; + if (!string.IsNullOrEmpty(equipment)) + return equipment; + return drawingNo ?? ""; + } + + private static string BuildDxfFileName(string drawingNo, string equipment, string itemNo) + { + var drawingNumber = BuildDrawingNumber(equipment, drawingNo); + var paddedItem = (itemNo ?? "").PadLeft(2, '0'); + if (!string.IsNullOrEmpty(drawingNumber) && !string.IsNullOrEmpty(itemNo)) + return $"{drawingNumber} PT{paddedItem}.dxf"; + return $"PT{paddedItem}.dxf"; + } + } +} diff --git a/FabWorks.Api/Services/FormProgramService.cs b/FabWorks.Api/Services/FormProgramService.cs new file mode 100644 index 0000000..01cb56e --- /dev/null +++ b/FabWorks.Api/Services/FormProgramService.cs @@ -0,0 +1,39 @@ +using FabWorks.Core.Models; +using FabWorks.Core.PressBrake; +using System.Security.Cryptography; + +namespace FabWorks.Api.Services +{ + public class FormProgramService + { + public FormProgram ParseFromFile(string filePath) + { + var pgm = FabWorks.Core.PressBrake.Program.Load(filePath); + var hash = ComputeFileHash(filePath); + + return new FormProgram + { + ProgramFilePath = filePath, + ContentHash = hash, + ProgramName = pgm.ProgName ?? "", + Thickness = pgm.MatThick > 0 ? pgm.MatThick : null, + MaterialType = pgm.MatType.ToString(), + KFactor = pgm.KFactor > 0 ? pgm.KFactor : null, + BendCount = pgm.Steps.Count, + UpperToolNames = string.Join(", ", pgm.UpperToolSets + .Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()), + LowerToolNames = string.Join(", ", pgm.LowerToolSets + .Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()), + SetupNotes = pgm.SetupNotes ?? "" + }; + } + + private static string ComputeFileHash(string filePath) + { + using var sha = SHA256.Create(); + using var stream = File.OpenRead(filePath); + var bytes = sha.ComputeHash(stream); + return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); + } + } +} diff --git a/FabWorks.Api/appsettings.Development.json b/FabWorks.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/FabWorks.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/FabWorks.Api/appsettings.json b/FabWorks.Api/appsettings.json new file mode 100644 index 0000000..b9b4456 --- /dev/null +++ b/FabWorks.Api/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "FabWorksDb": "Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;" + }, + "FileStorage": { + "OutputFolder": "C:\\ExportDXF\\Output" + } +} diff --git a/FabWorks.Api/wwwroot/css/styles.css b/FabWorks.Api/wwwroot/css/styles.css new file mode 100644 index 0000000..eadff8d --- /dev/null +++ b/FabWorks.Api/wwwroot/css/styles.css @@ -0,0 +1,745 @@ +:root { + --bg-deep: #f0f1f3; + --bg: #f8f9fa; + --surface: #ffffff; + --surface-raised: #ffffff; + --border: #d0d5dd; + --border-subtle: #e4e7ec; + --text: #1a1a1a; + --text-secondary: #475467; + --text-dim: #667085; + --cyan: #0975b0; + --cyan-dim: rgba(9, 117, 176, 0.1); + --cyan-glow: rgba(9, 117, 176, 0.2); + --amber: #b54708; + --amber-dim: rgba(181, 71, 8, 0.08); + --green: #067647; + --green-dim: rgba(6, 118, 71, 0.08); + --red: #d92d20; + --sidebar-w: 64px; + --font-display: 'Outfit', sans-serif; + --font-body: 'IBM Plex Sans', sans-serif; + --font-mono: 'IBM Plex Mono', monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: var(--font-body); + background: var(--bg); + color: var(--text); + display: flex; + min-height: 100vh; + overflow-x: hidden; +} + +/* ─── Sidebar ─── */ +.sidebar { + width: var(--sidebar-w); + background: var(--bg-deep); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + align-items: center; + position: fixed; + top: 0; left: 0; bottom: 0; + z-index: 50; + padding-top: 8px; +} + +.sidebar-brand { + width: 40px; height: 40px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; + position: relative; +} + +.sidebar-brand::after { + content: ''; + position: absolute; + bottom: -12px; + left: 8px; right: 8px; + height: 1px; + background: var(--border); +} + +.sidebar-brand svg { + width: 26px; height: 26px; + color: var(--cyan); +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 4px; + padding-top: 16px; + width: 100%; +} + +.nav-item { + display: flex; + align-items: center; + justify-content: center; + width: 44px; height: 44px; + margin: 0 auto; + color: var(--text-dim); + text-decoration: none; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s; + position: relative; +} + +.nav-item:hover { + color: var(--text-secondary); + background: var(--surface); +} + +.nav-item.active { + color: var(--cyan); + background: var(--cyan-dim); +} + +.nav-item.active::before { + content: ''; + position: absolute; + left: -10px; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 20px; + background: var(--cyan); + border-radius: 0 2px 2px 0; +} + +.nav-item svg { width: 20px; height: 20px; } + +.nav-tooltip { + position: absolute; + left: calc(100% + 12px); + top: 50%; + transform: translateY(-50%); + background: var(--text); + border: 1px solid var(--border); + color: #fff; + padding: 4px 10px; + border-radius: 4px; + font-size: 13px; + font-family: var(--font-body); + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; + z-index: 100; +} + +.nav-item:hover .nav-tooltip { opacity: 1; } + +/* ─── Main ─── */ +.main { + margin-left: var(--sidebar-w); + flex: 1; + display: flex; + flex-direction: column; + min-height: 100vh; + position: relative; + z-index: 1; +} + +.topbar { + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); + padding: 0 32px; + height: 56px; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 40; +} + +.topbar-left { + display: flex; + align-items: center; + gap: 12px; +} + +.topbar h2 { + font-family: var(--font-display); + font-size: 18px; + font-weight: 600; + letter-spacing: -0.01em; +} + +.topbar-tag { + font-family: var(--font-mono); + font-size: 12px; + color: var(--cyan); + background: var(--cyan-dim); + padding: 2px 8px; + border-radius: 3px; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.page-content { + padding: 28px 32px; + flex: 1; +} + +/* ─── Animations ─── */ +@keyframes fadeSlideIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-in { + animation: fadeSlideIn 0.3s ease forwards; + opacity: 0; +} + +.animate-in:nth-child(1) { animation-delay: 0.04s; } +.animate-in:nth-child(2) { animation-delay: 0.08s; } +.animate-in:nth-child(3) { animation-delay: 0.12s; } +.animate-in:nth-child(4) { animation-delay: 0.16s; } + +/* ─── Cards ─── */ +.card { + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 6px; + overflow: hidden; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); +} + +.card-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; + justify-content: space-between; + text-transform: uppercase; + color: var(--text-secondary); +} + +.card-body { padding: 18px; } + +/* ─── Stats ─── */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 6px; + padding: 18px 20px; + position: relative; + overflow: hidden; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; left: 0; + width: 100%; height: 3px; + background: linear-gradient(90deg, var(--cyan), transparent); + opacity: 0.5; +} + +.stat-label { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1.5px; +} + +.stat-value { + font-family: var(--font-display); + font-size: 32px; + font-weight: 700; + margin-top: 4px; + color: var(--text); + letter-spacing: -0.02em; +} + +.stat-value.stat-sm { + font-size: 15px; + font-weight: 500; + font-family: var(--font-mono); +} + +/* ─── Tables ─── */ +table { width: 100%; border-collapse: collapse; } + +th { + text-align: left; + padding: 10px 16px; + background: var(--bg); + border-bottom: 1px solid var(--border); + font-family: var(--font-mono); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-dim); + font-weight: 600; + white-space: nowrap; +} + +td { + padding: 12px 16px; + border-bottom: 1px solid var(--border-subtle); + font-size: 14px; +} + +tbody tr { transition: background 0.1s; } +tbody tr:hover td { background: var(--cyan-dim); } +tbody tr:last-child td { border-bottom: none; } + +/* ─── Badges ─── */ +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 3px; + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.badge svg { width: 14px; height: 14px; flex-shrink: 0; } +.badge-cyan { background: var(--cyan-dim); color: var(--cyan); } +.badge-amber { background: var(--amber-dim); color: var(--amber); } +.badge-green { background: var(--green-dim); color: var(--green); } +.badge-count { + background: var(--bg); + color: var(--text-secondary); + border: 1px solid var(--border); +} + +/* ─── Buttons ─── */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + border-radius: 4px; + font-family: var(--font-body); + font-size: 13px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text-secondary); + transition: all 0.15s; + white-space: nowrap; +} + +.btn:hover { + background: var(--bg); + color: var(--text); + border-color: var(--text-dim); +} + +.btn svg { width: 14px; height: 14px; } + +.btn-cyan { + background: var(--cyan-dim); + color: var(--cyan); + border-color: rgba(9, 117, 176, 0.25); +} + +.btn-cyan:hover { + background: rgba(9, 117, 176, 0.15); + border-color: rgba(9, 117, 176, 0.4); + color: var(--cyan); +} + +.btn-amber { + background: var(--amber-dim); + color: var(--amber); + border-color: rgba(181, 71, 8, 0.25); +} + +.btn-amber:hover { + background: rgba(181, 71, 8, 0.15); + border-color: rgba(181, 71, 8, 0.4); + 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 ─── */ +.search-box { + display: flex; + align-items: center; + gap: 8px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0 12px; + height: 36px; + width: 300px; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.search-box:focus-within { + border-color: var(--cyan); + box-shadow: 0 0 0 2px var(--cyan-dim); +} + +.search-box svg { + width: 16px; height: 16px; + color: var(--text-dim); + flex-shrink: 0; +} + +.search-box input { + border: none; + outline: none; + font-family: var(--font-body); + font-size: 14px; + width: 100%; + background: transparent; + color: var(--text); +} + +.search-box input::placeholder { color: var(--text-dim); } + +/* ─── Clickable ─── */ +.clickable { cursor: pointer; } + +/* ─── Detail sections ─── */ +.detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 20px; +} + +.detail-field label { + display: block; + font-family: var(--font-mono); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1.2px; + color: var(--text-dim); + margin-bottom: 4px; +} + +.detail-field .value { + font-size: 15px; + font-weight: 500; + word-break: break-all; +} + +.detail-field .value.mono { + font-family: var(--font-mono); + font-size: 13px; + color: var(--text-secondary); +} + +/* ─── Back link ─── */ +.back-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--text-dim); + text-decoration: none; + font-size: 13px; + cursor: pointer; + margin-bottom: 20px; + font-family: var(--font-mono); + text-transform: uppercase; + letter-spacing: 0.5px; + transition: color 0.15s; +} + +.back-link:hover { color: var(--cyan); } +.back-link svg { width: 14px; height: 14px; } + +/* ─── BOM Expansion ─── */ +.bom-expand-row td { + padding: 0 !important; + background: var(--bg) !important; +} + +.bom-expand-content { + padding: 16px 16px 16px 48px; + border-left: 3px solid var(--cyan-dim); + margin-left: 16px; +} + +.bom-expand-content .info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 6px 24px; +} + +.bom-expand-content .info-item { + font-size: 13px; + padding: 2px 0; +} + +.bom-expand-content .info-item .lbl { + color: var(--text-dim); + font-family: var(--font-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-right: 6px; +} + +.bom-expand-content .info-item .val { + font-family: var(--font-mono); + color: var(--text); +} + +.bom-section-title { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1.5px; + color: var(--cyan); + margin: 14px 0 8px; + display: flex; + align-items: center; + gap: 8px; +} + +.bom-section-title svg { width: 14px; height: 14px; flex-shrink: 0; } +.bom-section-title:first-child { margin-top: 0; } + +.bom-section-title::after { + content: ''; + flex: 1; + height: 1px; + background: var(--border-subtle); +} + +/* ─── File Browser ─── */ +.breadcrumb { + display: flex; + align-items: center; + gap: 6px; + font-family: var(--font-mono); + font-size: 13px; + margin-bottom: 16px; + flex-wrap: wrap; + padding: 8px 14px; + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 4px; +} + +.breadcrumb a { + color: var(--cyan); + text-decoration: none; + cursor: pointer; + transition: opacity 0.15s; +} + +.breadcrumb a:hover { opacity: 0.7; } +.breadcrumb .sep { color: var(--text-dim); font-size: 11px; } +.breadcrumb .current { color: var(--text); font-weight: 500; } + +.file-name-cell { + display: flex; + align-items: center; + gap: 10px; +} + +.file-name-cell svg { width: 18px; height: 18px; flex-shrink: 0; } + +.file-name-cell a { + color: var(--text); + text-decoration: none; + cursor: pointer; + transition: color 0.15s; +} + +.file-name-cell a:hover { color: var(--cyan); } + +/* ─── Loading / Empty ─── */ +.loading, .empty { + text-align: center; + padding: 60px 24px; + color: var(--text-dim); + font-size: 14px; + font-family: var(--font-mono); +} + +.loading::before { + content: ''; + display: block; + width: 24px; + height: 24px; + border: 2px solid var(--border); + border-top-color: var(--cyan); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin: 0 auto 12px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ─── Chevron toggle ─── */ +.chevron-toggle { + display: inline-flex; + width: 18px; height: 18px; + align-items: center; + justify-content: center; + transition: transform 0.2s; + color: var(--text-dim); +} + +.chevron-toggle.open { + transform: rotate(90deg); + color: var(--cyan); +} + +/* ─── Drawing cards grid ─── */ +.drawings-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; +} + +.drawing-card { + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 6px; + padding: 18px 20px; + cursor: pointer; + transition: all 0.2s; + position: relative; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); +} + +.drawing-card:hover { + border-color: var(--cyan); + background: var(--cyan-dim); + box-shadow: 0 2px 8px rgba(9, 117, 176, 0.1); +} + +.drawing-card-title { + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; + margin-bottom: 2px; +} + +.drawing-card-sub { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; +} + +/* ─── Equipment Groups ─── */ +.equip-group { + margin-bottom: 16px; +} + +.equip-group:last-child { margin-bottom: 0; } + +.equip-header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 6px 6px 0 0; + cursor: pointer; + transition: all 0.15s; + user-select: none; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); +} + +.equip-header:hover { background: var(--cyan-dim); } + +.equip-header .chevron-toggle { flex-shrink: 0; } + +.equip-header-title { + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; +} + +.equip-header-number { + font-family: var(--font-mono); + font-size: 15px; + color: var(--cyan); + font-weight: 600; +} + +.equip-header-meta { + margin-left: auto; + display: flex; + align-items: center; + gap: 12px; +} + +.equip-header-stat { + font-family: var(--font-mono); + font-size: 13px; + color: var(--text-dim); +} + +.equip-header-stat strong { + color: var(--text-secondary); +} + +.equip-body { + border: 1px solid var(--border-subtle); + border-top: none; + border-radius: 0 0 6px 6px; + overflow: hidden; +} + +.equip-body table { margin: 0; } + +.equip-group.collapsed .equip-body { display: none; } +.equip-group.collapsed .equip-header { border-radius: 6px; } + +/* ─── Responsive ─── */ +@media (max-width: 768px) { + .sidebar { display: none; } + .main { margin-left: 0; } + .search-box { width: 100%; } + .topbar { padding: 0 16px; } + .page-content { padding: 16px; } +} diff --git a/FabWorks.Api/wwwroot/index.html b/FabWorks.Api/wwwroot/index.html new file mode 100644 index 0000000..4b1b4c4 --- /dev/null +++ b/FabWorks.Api/wwwroot/index.html @@ -0,0 +1,56 @@ + + + + + + FabWorks + + + + + + + + +
+
+
+

Exports

+ +
+
+
+
+
+ + + + + + + + + diff --git a/FabWorks.Api/wwwroot/js/components.js b/FabWorks.Api/wwwroot/js/components.js new file mode 100644 index 0000000..fac1db5 --- /dev/null +++ b/FabWorks.Api/wwwroot/js/components.js @@ -0,0 +1,60 @@ +/* ─── BOM Detail Expansion ─── */ +function renderBomDetails(b) { + let html = '
'; + + if (b.cutTemplate) { + const ct = b.cutTemplate; + const displayName = ct.dxfFilePath?.split(/[/\\]/).pop() || ''; + html += ` +
${icons.laser} Cut Template
+
+
File${esc(displayName)}
+
Thickness${fmtThickness(ct.thickness)}
+
K-Factor${ct.kFactor != null ? ct.kFactor : '\u2014'}
+
Bend Radius${ct.defaultBendRadius != null ? ct.defaultBendRadius.toFixed(4) + '"' : '\u2014'}
+
`; + + if (ct.contentHash) { + html += `
+ ${icons.download} Download DXF + ${esc(displayName)} +
`; + } + } + + if (b.formProgram) { + const fp = b.formProgram; + html += ` +
${icons.bend} Form Program
+
+
Program${esc(fp.programName)}
+
Thickness${fmtThickness(fp.thickness)}
+
Material${esc(fp.materialType)}
+
K-Factor${fp.kFactor != null ? fp.kFactor : '\u2014'}
+
Bends${fp.bendCount}
+
Upper Tools${esc(fp.upperToolNames) || '\u2014'}
+
Lower Tools${esc(fp.lowerToolNames) || '\u2014'}
+
+ ${fp.setupNotes ? `
Setup Notes${esc(fp.setupNotes)}
` : ''}`; + } + + html += '
'; + return html; +} + +function toggleEquipGroup(id) { + const group = document.getElementById(id); + const icon = document.getElementById(id + '-icon'); + if (!group) return; + group.classList.toggle('collapsed'); + if (icon) icon.classList.toggle('open', !group.classList.contains('collapsed')); +} + +function toggleBomRow(id) { + const row = document.getElementById(id); + const icon = document.getElementById(id + '-icon'); + if (!row) return; + const visible = row.style.display !== 'none'; + row.style.display = visible ? 'none' : ''; + if (icon) icon.classList.toggle('open', !visible); +} diff --git a/FabWorks.Api/wwwroot/js/helpers.js b/FabWorks.Api/wwwroot/js/helpers.js new file mode 100644 index 0000000..787225f --- /dev/null +++ b/FabWorks.Api/wwwroot/js/helpers.js @@ -0,0 +1,50 @@ +function fmtSize(b) { + if (!b) return '0 B'; + const k = 1024, s = ['B','KB','MB','GB']; + const i = Math.floor(Math.log(b) / Math.log(k)); + return parseFloat((b / Math.pow(k, i)).toFixed(1)) + ' ' + s[i]; +} + +function fmtDate(d) { + if (!d) return ''; + const dt = new Date(d); + return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + + ' ' + dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +function fmtThickness(t) { + if (t == null) return '\u2014'; + return `${t.toFixed(4)}"`; +} + +function esc(s) { + return s ? s.replace(//g,'>').replace(/"/g,'"').replace(/'/g,''') : ''; +} + +function setPage(title, tag = '') { + document.getElementById('page-title').textContent = title; + document.getElementById('page-tag').textContent = tag; + document.getElementById('page-tag').style.display = tag ? '' : 'none'; +} + +const api = { + async get(url) { + const r = await fetch(url); + if (!r.ok) throw new Error(`${r.status} ${r.statusText}`); + return r.json(); + }, + async del(url) { + const r = await fetch(url, { method: 'DELETE' }); + if (!r.ok) throw new Error(`${r.status} ${r.statusText}`); + } +}; + +async function deleteExport(id) { + if (!confirm('Delete this export record? This cannot be undone.')) return; + try { + await api.del(`/api/exports/${id}`); + router.dispatch(); + } catch (err) { + alert('Failed to delete: ' + err.message); + } +} diff --git a/FabWorks.Api/wwwroot/js/icons.js b/FabWorks.Api/wwwroot/js/icons.js new file mode 100644 index 0000000..5af4ad1 --- /dev/null +++ b/FabWorks.Api/wwwroot/js/icons.js @@ -0,0 +1,20 @@ +const icons = { + search: ``, + folder: ``, + fileDxf: ``, + filePdf: ``, + fileGeneric: ``, + download: ``, + back: ``, + chevron: ``, + laser: ``, + bend: ``, + trash: ``, +}; + +function fileIcon(name) { + const ext = name.split('.').pop().toLowerCase(); + if (ext === 'dxf') return icons.fileDxf; + if (ext === 'pdf') return icons.filePdf; + return icons.fileGeneric; +} diff --git a/FabWorks.Api/wwwroot/js/pages.js b/FabWorks.Api/wwwroot/js/pages.js new file mode 100644 index 0000000..c89b136 --- /dev/null +++ b/FabWorks.Api/wwwroot/js/pages.js @@ -0,0 +1,428 @@ +const pages = { + async exports(params) { + const actions = document.getElementById('topbar-actions'); + const content = document.getElementById('page-content'); + setPage('Exports'); + + const searchVal = params.q || ''; + actions.innerHTML = ` + `; + + content.innerHTML = `
Loading exports
`; + + const searchInput = document.getElementById('export-search'); + let debounce; + searchInput.addEventListener('input', () => { + clearTimeout(debounce); + debounce = setTimeout(() => router.go('exports', { q: searchInput.value }), 400); + }); + + try { + const searchQ = searchVal ? `&search=${encodeURIComponent(searchVal)}` : ''; + const data = await api.get(`/api/exports?take=500${searchQ}`); + + if (data.items.length === 0) { + content.innerHTML = `
No exports found.
`; + return; + } + + setPage('Exports', `${data.items.length} exports`); + + const rows = data.items.map((e, i) => ` + + ${e.id} + ${esc(e.drawingNumber) || '\u2014'} + ${esc(e.title) || ''} + ${e.bomItemCount} + ${esc(e.exportedBy)} + ${fmtDate(e.exportedAt)} + + `).join(''); + + content.innerHTML = ` +
+ + + + + + + + + + + ${rows} +
#DrawingTitleItemsExported ByDate
+
`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + }, + + async exportDetail(id) { + const actions = document.getElementById('topbar-actions'); + const content = document.getElementById('page-content'); + setPage('Loading...'); + actions.innerHTML = ''; + content.innerHTML = `
Loading export
`; + + try { + const exp = await api.get(`/api/exports/${id}`); + setPage(exp.drawingNumber || `Export #${exp.id}`, 'export detail'); + + const dxfCount = (exp.bomItems || []).filter(b => b.cutTemplate?.contentHash).length; + + const bomRows = (exp.bomItems || []).map((b, i) => { + const hasDetails = b.cutTemplate || b.formProgram; + const toggleId = `bom-${b.id}`; + return ` + + ${hasDetails ? `${icons.chevron}` : ''} + ${esc(b.itemNo)} + ${esc(b.partName)} + ${esc(b.description)} + ${esc(b.material)} + ${b.qty ?? ''} + ${b.totalQty ?? ''} + + ${b.cutTemplate ? `${icons.laser} DXF` : ''} + ${b.formProgram ? `${icons.bend} Form` : ''} + + + ${hasDetails ? `${renderBomDetails(b)}` : ''}`; + }).join(''); + + content.innerHTML = ` + ${icons.back} Back to exports + +
+
Export Information
+
+
+
${esc(exp.drawingNumber) || '\u2014'}
+ ${exp.title ? `
${esc(exp.title)}
` : ''} +
${esc(exp.exportedBy)}
+
${fmtDate(exp.exportedAt)}
+
${esc(exp.sourceFilePath)}
+
+
+
+ +
+
+ BOM Items + ${exp.bomItems?.length || 0} items + + ${exp.pdfContentHash ? `${icons.download} PDF` : ''} + ${dxfCount > 0 ? `${icons.download} All DXFs` : ''} + + +
+ ${exp.bomItems?.length ? ` + + + + + + + + + + + + ${bomRows} +
ItemPart NameDescriptionMaterialQtyTotalData
` : '
No BOM items for this export.
'} +
`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + }, + + async drawings(params) { + const actions = document.getElementById('topbar-actions'); + const content = document.getElementById('page-content'); + setPage('Drawings'); + + const searchVal = (params && params.q) || ''; + actions.innerHTML = ` + `; + + content.innerHTML = `
Loading drawings
`; + + const searchInput = document.getElementById('drawing-search'); + let debounce; + searchInput.addEventListener('input', () => { + clearTimeout(debounce); + debounce = setTimeout(() => router.go('drawings', { q: searchInput.value }), 400); + }); + + try { + const searchQ = searchVal ? `&search=${encodeURIComponent(searchVal)}` : ''; + const data = await api.get(`/api/exports?take=500${searchQ}`); + + if (data.items.length === 0) { + content.innerHTML = `
No drawings found.
`; + return; + } + + // Deduplicate: keep only the latest export per drawing number + const seen = new Set(); + const unique = data.items.filter(e => { + const dn = e.drawingNumber || ''; + if (seen.has(dn)) return false; + seen.add(dn); + return true; + }); + + // Group by equipment number (first token of drawing number) + const groups = new Map(); + unique.forEach(e => { + const dn = e.drawingNumber || ''; + const spaceIdx = dn.indexOf(' '); + const equip = spaceIdx > 0 ? dn.substring(0, spaceIdx) : (dn || 'Other'); + if (!groups.has(equip)) groups.set(equip, []); + groups.get(equip).push(e); + }); + + // Sort equipment groups by number descending (most recent equipment first) + const sortedGroups = [...groups.entries()].sort((a, b) => { + const numA = parseInt(a[0]) || 0; + const numB = parseInt(b[0]) || 0; + return numB - numA; + }); + + const uniqueEquip = sortedGroups.length; + const uniqueDrawings = unique.length; + setPage('Drawings', `${uniqueDrawings} drawings / ${uniqueEquip} equipment`); + + const groupsHtml = sortedGroups.map(([equip, items], gi) => { + const totalBom = items.reduce((s, e) => s + e.bomItemCount, 0); + + const rows = items.map((e, i) => { + const dn = e.drawingNumber || ''; + const spaceIdx = dn.indexOf(' '); + const drawingPart = spaceIdx > 0 ? dn.substring(spaceIdx + 1) : dn; + + return ` + + ${esc(drawingPart) || '\u2014'} + ${esc(e.title) || ''} + ${e.bomItemCount} + ${esc(e.exportedBy)} + ${fmtDate(e.exportedAt)} + `; + }).join(''); + + return ` +
+
+ ${icons.chevron} + ${esc(equip)} +
+ ${items.length} drawings + ${totalBom} items +
+
+
+ + + + + + + + + ${rows} +
DrawingTitleItemsExported ByLatest Export
+
+
`; + }).join(''); + + content.innerHTML = ` +
+
Drawings
${uniqueDrawings}
+
Equipment
${uniqueEquip}
+
+ ${groupsHtml}`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + }, + + async drawingDetail(drawingEncoded) { + const drawingNumber = decodeURIComponent(drawingEncoded); + const actions = document.getElementById('topbar-actions'); + const content = document.getElementById('page-content'); + setPage(drawingNumber, 'drawing'); + actions.innerHTML = ''; + content.innerHTML = `
Loading drawing
`; + + try { + const exports = await api.get(`/api/exports/by-drawing?drawingNumber=${encodeURIComponent(drawingNumber)}`); + + if (exports.length === 0) { + content.innerHTML = ` + ${icons.back} Back to drawings +
No exports found for this drawing.
`; + return; + } + + const allBom = []; + exports.forEach(exp => { + (exp.bomItems || []).forEach(b => { + allBom.push({ ...b, exportId: exp.id, exportedAt: exp.exportedAt }); + }); + }); + + const bomRows = allBom.map((b, i) => { + const hasDetails = b.cutTemplate || b.formProgram; + const toggleId = `dbom-${b.id}`; + return ` + + ${hasDetails ? `${icons.chevron}` : ''} + ${esc(b.itemNo)} + ${esc(b.partName)} + ${esc(b.description)} + ${esc(b.material)} + ${b.qty ?? ''} + ${b.totalQty ?? ''} + + ${b.cutTemplate ? `${icons.laser} DXF` : ''} + ${b.formProgram ? `${icons.bend} Form` : ''} + + + ${hasDetails ? `${renderBomDetails(b)}` : ''}`; + }).join(''); + + content.innerHTML = ` + ${icons.back} Back to drawings + +
+
Exports
${exports.length}
+
BOM Items
${allBom.length}
+
Latest Export
${fmtDate(exports[0].exportedAt)}
+
+ +
+
+ All BOM Items + ${allBom.length} items +
+ ${allBom.length ? ` + + + + + + + + + + + + ${bomRows} +
ItemPart NameDescriptionMaterialQtyTotalData
` : '
No BOM items.
'} +
`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + }, + + async files(params) { + const actions = document.getElementById('topbar-actions'); + const content = document.getElementById('page-content'); + setPage('Files'); + + const searchVal = params.q || ''; + actions.innerHTML = ` +
+ + +
`; + + content.innerHTML = `
Loading files
`; + + const searchInput = document.getElementById('file-search'); + const typeFilter = document.getElementById('file-type-filter'); + let debounce; + const refresh = () => { + clearTimeout(debounce); + debounce = setTimeout(() => router.go('files', { q: searchInput.value + (typeFilter.value ? '&type=' + typeFilter.value : '') }), 400); + }; + searchInput.addEventListener('input', refresh); + typeFilter.addEventListener('change', refresh); + + // Parse search and type from combined param + let searchQ = searchVal; + let typeQ = ''; + if (searchVal.includes('&type=')) { + const parts = searchVal.split('&type='); + searchQ = parts[0]; + typeQ = parts[1] || ''; + searchInput.value = searchQ; + typeFilter.value = typeQ; + } + + try { + let url = '/api/filebrowser/files?'; + if (searchQ) url += `search=${encodeURIComponent(searchQ)}&`; + if (typeQ) url += `type=${encodeURIComponent(typeQ)}&`; + const data = await api.get(url); + + setPage('Files', `${data.total} files`); + + if (data.files.length === 0) { + content.innerHTML = `
No files found.
`; + return; + } + + const rows = data.files.map((f, i) => { + const ext = f.fileType || f.fileName.split('.').pop().toLowerCase(); + const hashShort = f.contentHash ? f.contentHash.substring(0, 12) : ''; + return ` + +
${ext === 'pdf' ? icons.filePdf : icons.fileDxf}${esc(f.fileName)}
+ ${ext.toUpperCase()} + ${esc(f.drawingNumber)} + ${f.thickness != null ? f.thickness.toFixed(4) + '"' : '\u2014'} + ${fmtDate(f.createdAt)} + ${esc(hashShort)} + + ${icons.download} + + `; + }).join(''); + + content.innerHTML = ` +
+ + + + + + + + + + + ${rows} +
NameTypeDrawingThicknessDateHashActions
+
`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + } +}; diff --git a/FabWorks.Api/wwwroot/js/router.js b/FabWorks.Api/wwwroot/js/router.js new file mode 100644 index 0000000..66fdfc1 --- /dev/null +++ b/FabWorks.Api/wwwroot/js/router.js @@ -0,0 +1,35 @@ +const router = { + go(page, params = {}) { + const hash = page + (params.id ? '/' + params.id : '') + (params.q ? '?q=' + encodeURIComponent(params.q) : ''); + location.hash = hash; + }, + parse() { + const h = location.hash.slice(1) || 'exports'; + const [path, qs] = h.split('?'); + const parts = path.split('/'); + const params = {}; + if (qs) qs.split('&').forEach(p => { const [k,v] = p.split('='); params[k] = decodeURIComponent(v); }); + return { page: parts[0], id: parts[1], params }; + }, + init() { + window.addEventListener('hashchange', () => this.dispatch()); + this.dispatch(); + }, + dispatch() { + const { page, id, params } = this.parse(); + document.querySelectorAll('.nav-item').forEach(el => { + el.classList.toggle('active', + el.dataset.page === page || + (page === 'export-detail' && el.dataset.page === 'exports') || + (page === 'drawing-detail' && el.dataset.page === 'drawings')); + }); + switch(page) { + case 'exports': pages.exports(params); break; + case 'export-detail': pages.exportDetail(id); break; + case 'drawings': pages.drawings(params); break; + case 'drawing-detail': pages.drawingDetail(id, params); break; + case 'files': pages.files(params); break; + default: pages.exports(params); + } + } +}; diff --git a/ExportDXF/Data/ExportDxfDbContext.cs b/FabWorks.Core/Data/FabWorksDbContext.cs similarity index 60% rename from ExportDXF/Data/ExportDxfDbContext.cs rename to FabWorks.Core/Data/FabWorksDbContext.cs index b0024ac..f2d6a40 100644 --- a/ExportDXF/Data/ExportDxfDbContext.cs +++ b/FabWorks.Core/Data/FabWorksDbContext.cs @@ -1,32 +1,16 @@ -using ExportDXF.Models; +using FabWorks.Core.Models; using Microsoft.EntityFrameworkCore; -using System.Configuration; -namespace ExportDXF.Data +namespace FabWorks.Core.Data { - public class ExportDxfDbContext : DbContext + public class FabWorksDbContext : DbContext { public DbSet ExportRecords { get; set; } public DbSet BomItems { get; set; } public DbSet CutTemplates { get; set; } + public DbSet FormPrograms { get; set; } - public ExportDxfDbContext() : base() - { - } - - public ExportDxfDbContext(DbContextOptions options) : base(options) - { - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (!optionsBuilder.IsConfigured) - { - var connectionString = ConfigurationManager.ConnectionStrings["ExportDxfDb"]?.ConnectionString - ?? "Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;"; - optionsBuilder.UseSqlServer(connectionString); - } - } + public FabWorksDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -36,6 +20,9 @@ namespace ExportDXF.Data { entity.HasKey(e => e.Id); entity.Property(e => e.DrawingNumber).HasMaxLength(100); + entity.Property(e => e.Title).HasMaxLength(200); + entity.Property(e => e.EquipmentNo).HasMaxLength(50); + entity.Property(e => e.DrawingNo).HasMaxLength(50); entity.Property(e => e.SourceFilePath).HasMaxLength(500); entity.Property(e => e.OutputFolder).HasMaxLength(500); entity.Property(e => e.ExportedBy).HasMaxLength(100); @@ -61,14 +48,32 @@ namespace ExportDXF.Data .WithOne(ct => ct.BomItem) .HasForeignKey(ct => ct.BomItemId) .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.FormProgram) + .WithOne(fp => fp.BomItem) + .HasForeignKey(fp => fp.BomItemId) + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); + entity.Property(e => e.DxfFilePath).HasMaxLength(500); entity.Property(e => e.CutTemplateName).HasMaxLength(100); entity.Property(e => e.ContentHash).HasMaxLength(64); }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.ProgramFilePath).HasMaxLength(500); + entity.Property(e => e.ContentHash).HasMaxLength(64); + entity.Property(e => e.ProgramName).HasMaxLength(200); + entity.Property(e => e.MaterialType).HasMaxLength(50); + entity.Property(e => e.UpperToolNames).HasMaxLength(500); + entity.Property(e => e.LowerToolNames).HasMaxLength(500); + entity.Property(e => e.SetupNotes).HasMaxLength(2000); + }); } } } diff --git a/FabWorks.Core/FabWorks.Core.csproj b/FabWorks.Core/FabWorks.Core.csproj new file mode 100644 index 0000000..a82a833 --- /dev/null +++ b/FabWorks.Core/FabWorks.Core.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + disable + disable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.Designer.cs b/FabWorks.Core/Migrations/20260218171742_InitialCreate.Designer.cs similarity index 58% rename from ExportDXF/Migrations/20260214195856_ExtractCutTemplate.Designer.cs rename to FabWorks.Core/Migrations/20260218171742_InitialCreate.Designer.cs index 6c06ec6..2b8a06c 100644 --- a/ExportDXF/Migrations/20260214195856_ExtractCutTemplate.Designer.cs +++ b/FabWorks.Core/Migrations/20260218171742_InitialCreate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using ExportDXF.Data; +using FabWorks.Core.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -9,11 +9,11 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace ExportDXF.Migrations +namespace FabWorks.Core.Migrations { - [DbContext(typeof(ExportDxfDbContext))] - [Migration("20260214195856_ExtractCutTemplate")] - partial class ExtractCutTemplate + [DbContext(typeof(FabWorksDbContext))] + [Migration("20260218171742_InitialCreate")] + partial class InitialCreate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -25,7 +25,7 @@ namespace ExportDXF.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("ExportDXF.Models.BomItem", b => + modelBuilder.Entity("FabWorks.Core.Models.BomItem", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -76,7 +76,7 @@ namespace ExportDXF.Migrations b.ToTable("BomItems"); }); - modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => + modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -99,7 +99,8 @@ namespace ExportDXF.Migrations .HasColumnType("float"); b.Property("DxfFilePath") - .HasColumnType("nvarchar(max)"); + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); b.Property("KFactor") .HasColumnType("float"); @@ -115,7 +116,7 @@ namespace ExportDXF.Migrations b.ToTable("CutTemplates"); }); - modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => + modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -123,10 +124,18 @@ namespace ExportDXF.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("DrawingNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + b.Property("DrawingNumber") .HasMaxLength(100) .HasColumnType("nvarchar(100)"); + b.Property("EquipmentNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + b.Property("ExportedAt") .HasColumnType("datetime2"); @@ -146,14 +155,74 @@ namespace ExportDXF.Migrations .HasMaxLength(500) .HasColumnType("nvarchar(500)"); + b.Property("Title") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + b.HasKey("Id"); b.ToTable("ExportRecords"); }); - modelBuilder.Entity("ExportDXF.Models.BomItem", b => + modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b => { - b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord") + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BendCount") + .HasColumnType("int"); + + b.Property("BomItemId") + .HasColumnType("int"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("KFactor") + .HasColumnType("float"); + + b.Property("LowerToolNames") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("MaterialType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProgramFilePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ProgramName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SetupNotes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Thickness") + .HasColumnType("float"); + + b.Property("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) @@ -162,23 +231,36 @@ namespace ExportDXF.Migrations b.Navigation("ExportRecord"); }); - modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => + modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b => { - b.HasOne("ExportDXF.Models.BomItem", "BomItem") + b.HasOne("FabWorks.Core.Models.BomItem", "BomItem") .WithOne("CutTemplate") - .HasForeignKey("ExportDXF.Models.CutTemplate", "BomItemId") + .HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("BomItem"); }); - modelBuilder.Entity("ExportDXF.Models.BomItem", b => + modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b => { - b.Navigation("CutTemplate"); + b.HasOne("FabWorks.Core.Models.BomItem", "BomItem") + .WithOne("FormProgram") + .HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BomItem"); }); - modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => + modelBuilder.Entity("FabWorks.Core.Models.BomItem", b => + { + b.Navigation("CutTemplate"); + + b.Navigation("FormProgram"); + }); + + modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b => { b.Navigation("BomItems"); }); diff --git a/FabWorks.Core/Migrations/20260218171742_InitialCreate.cs b/FabWorks.Core/Migrations/20260218171742_InitialCreate.cs new file mode 100644 index 0000000..77ae070 --- /dev/null +++ b/FabWorks.Core/Migrations/20260218171742_InitialCreate.cs @@ -0,0 +1,151 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FabWorks.Core.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ExportRecords", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DrawingNumber = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + Title = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + EquipmentNo = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + DrawingNo = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + SourceFilePath = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + OutputFolder = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + ExportedAt = table.Column(type: "datetime2", nullable: false), + ExportedBy = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + PdfContentHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExportRecords", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BomItems", + columns: table => new + { + ID = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ItemNo = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + PartNo = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + SortOrder = table.Column(type: "int", nullable: false), + Qty = table.Column(type: "int", nullable: true), + TotalQty = table.Column(type: "int", nullable: true), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + PartName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + ConfigurationName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + Material = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + ExportRecordId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BomItems", x => x.ID); + table.ForeignKey( + name: "FK_BomItems_ExportRecords_ExportRecordId", + column: x => x.ExportRecordId, + principalTable: "ExportRecords", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CutTemplates", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DxfFilePath = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + ContentHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + CutTemplateName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + Thickness = table.Column(type: "float", nullable: true), + KFactor = table.Column(type: "float", nullable: true), + DefaultBendRadius = table.Column(type: "float", nullable: true), + BomItemId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CutTemplates", x => x.Id); + table.ForeignKey( + name: "FK_CutTemplates_BomItems_BomItemId", + column: x => x.BomItemId, + principalTable: "BomItems", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FormPrograms", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ProgramFilePath = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + ContentHash = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + ProgramName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + Thickness = table.Column(type: "float", nullable: true), + MaterialType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + KFactor = table.Column(type: "float", nullable: true), + BendCount = table.Column(type: "int", nullable: false), + UpperToolNames = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + LowerToolNames = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + SetupNotes = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + BomItemId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FormPrograms", x => x.Id); + table.ForeignKey( + name: "FK_FormPrograms_BomItems_BomItemId", + column: x => x.BomItemId, + principalTable: "BomItems", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_BomItems_ExportRecordId", + table: "BomItems", + column: "ExportRecordId"); + + migrationBuilder.CreateIndex( + name: "IX_CutTemplates_BomItemId", + table: "CutTemplates", + column: "BomItemId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FormPrograms_BomItemId", + table: "FormPrograms", + column: "BomItemId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CutTemplates"); + + migrationBuilder.DropTable( + name: "FormPrograms"); + + migrationBuilder.DropTable( + name: "BomItems"); + + migrationBuilder.DropTable( + name: "ExportRecords"); + } + } +} diff --git a/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs b/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs new file mode 100644 index 0000000..59ad995 --- /dev/null +++ b/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs @@ -0,0 +1,273 @@ +// +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("20260219134027_AddCutTemplateRevision")] + partial class AddCutTemplateRevision + { + /// + 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("ID") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ID")); + + b.Property("ConfigurationName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ExportRecordId") + .HasColumnType("int"); + + b.Property("ItemNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Material") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PartNo") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Qty") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TotalQty") + .HasColumnType("int"); + + b.HasKey("ID"); + + b.HasIndex("ExportRecordId"); + + b.ToTable("BomItems"); + }); + + modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BomItemId") + .HasColumnType("int"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CutTemplateName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DefaultBendRadius") + .HasColumnType("float"); + + b.Property("DxfFilePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("KFactor") + .HasColumnType("float"); + + b.Property("Revision") + .HasColumnType("int"); + + b.Property("Thickness") + .HasColumnType("float"); + + b.HasKey("Id"); + + b.HasIndex("BomItemId") + .IsUnique(); + + b.ToTable("CutTemplates"); + }); + + modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DrawingNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DrawingNumber") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EquipmentNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ExportedAt") + .HasColumnType("datetime2"); + + b.Property("ExportedBy") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OutputFolder") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PdfContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("SourceFilePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Title") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("ExportRecords"); + }); + + modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BendCount") + .HasColumnType("int"); + + b.Property("BomItemId") + .HasColumnType("int"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("KFactor") + .HasColumnType("float"); + + b.Property("LowerToolNames") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("MaterialType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProgramFilePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ProgramName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SetupNotes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Thickness") + .HasColumnType("float"); + + b.Property("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.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.ExportRecord", b => + { + b.Navigation("BomItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.cs b/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.cs new file mode 100644 index 0000000..4f8d959 --- /dev/null +++ b/FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FabWorks.Core.Migrations +{ + /// + public partial class AddCutTemplateRevision : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Revision", + table: "CutTemplates", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Revision", + table: "CutTemplates"); + } + } +} diff --git a/ExportDXF/Migrations/ExportDxfDbContextModelSnapshot.cs b/FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs similarity index 57% rename from ExportDXF/Migrations/ExportDxfDbContextModelSnapshot.cs rename to FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs index 130ebad..c6ebca9 100644 --- a/ExportDXF/Migrations/ExportDxfDbContextModelSnapshot.cs +++ b/FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ // using System; -using ExportDXF.Data; +using FabWorks.Core.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -8,10 +8,10 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace ExportDXF.Migrations +namespace FabWorks.Core.Migrations { - [DbContext(typeof(ExportDxfDbContext))] - partial class ExportDxfDbContextModelSnapshot : ModelSnapshot + [DbContext(typeof(FabWorksDbContext))] + partial class FabWorksDbContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { @@ -22,7 +22,7 @@ namespace ExportDXF.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("ExportDXF.Models.BomItem", b => + modelBuilder.Entity("FabWorks.Core.Models.BomItem", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -73,7 +73,7 @@ namespace ExportDXF.Migrations b.ToTable("BomItems"); }); - modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => + modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -96,11 +96,15 @@ namespace ExportDXF.Migrations .HasColumnType("float"); b.Property("DxfFilePath") - .HasColumnType("nvarchar(max)"); + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); b.Property("KFactor") .HasColumnType("float"); + b.Property("Revision") + .HasColumnType("int"); + b.Property("Thickness") .HasColumnType("float"); @@ -112,7 +116,7 @@ namespace ExportDXF.Migrations b.ToTable("CutTemplates"); }); - modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => + modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -120,10 +124,18 @@ namespace ExportDXF.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("DrawingNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + b.Property("DrawingNumber") .HasMaxLength(100) .HasColumnType("nvarchar(100)"); + b.Property("EquipmentNo") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + b.Property("ExportedAt") .HasColumnType("datetime2"); @@ -143,14 +155,74 @@ namespace ExportDXF.Migrations .HasMaxLength(500) .HasColumnType("nvarchar(500)"); + b.Property("Title") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + b.HasKey("Id"); b.ToTable("ExportRecords"); }); - modelBuilder.Entity("ExportDXF.Models.BomItem", b => + modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b => { - b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord") + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BendCount") + .HasColumnType("int"); + + b.Property("BomItemId") + .HasColumnType("int"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("KFactor") + .HasColumnType("float"); + + b.Property("LowerToolNames") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("MaterialType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProgramFilePath") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ProgramName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SetupNotes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Thickness") + .HasColumnType("float"); + + b.Property("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) @@ -159,23 +231,36 @@ namespace ExportDXF.Migrations b.Navigation("ExportRecord"); }); - modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => + modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b => { - b.HasOne("ExportDXF.Models.BomItem", "BomItem") + b.HasOne("FabWorks.Core.Models.BomItem", "BomItem") .WithOne("CutTemplate") - .HasForeignKey("ExportDXF.Models.CutTemplate", "BomItemId") + .HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("BomItem"); }); - modelBuilder.Entity("ExportDXF.Models.BomItem", b => + modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b => { - b.Navigation("CutTemplate"); + b.HasOne("FabWorks.Core.Models.BomItem", "BomItem") + .WithOne("FormProgram") + .HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BomItem"); }); - modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => + modelBuilder.Entity("FabWorks.Core.Models.BomItem", b => + { + b.Navigation("CutTemplate"); + + b.Navigation("FormProgram"); + }); + + modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b => { b.Navigation("BomItems"); }); diff --git a/FabWorks.Core/Models/BomItem.cs b/FabWorks.Core/Models/BomItem.cs new file mode 100644 index 0000000..863eb47 --- /dev/null +++ b/FabWorks.Core/Models/BomItem.cs @@ -0,0 +1,22 @@ +namespace FabWorks.Core.Models +{ + public class BomItem + { + public int ID { get; set; } + public string ItemNo { get; set; } = ""; + public string PartNo { get; set; } = ""; + public int SortOrder { get; set; } + public int? Qty { get; set; } + public int? TotalQty { get; set; } + public string Description { get; set; } = ""; + public string PartName { get; set; } = ""; + public string ConfigurationName { get; set; } = ""; + public string Material { get; set; } = ""; + + public int ExportRecordId { get; set; } + public virtual ExportRecord ExportRecord { get; set; } + + public virtual CutTemplate CutTemplate { get; set; } + public virtual FormProgram FormProgram { get; set; } + } +} diff --git a/FabWorks.Core/Models/CutTemplate.cs b/FabWorks.Core/Models/CutTemplate.cs new file mode 100644 index 0000000..db0c9eb --- /dev/null +++ b/FabWorks.Core/Models/CutTemplate.cs @@ -0,0 +1,33 @@ +using System; + +namespace FabWorks.Core.Models +{ + public class CutTemplate + { + public int Id { get; set; } + public string DxfFilePath { get; set; } = ""; + public string ContentHash { get; set; } + public string CutTemplateName { get; set; } = ""; + + private double? _thickness; + public double? Thickness + { + get => _thickness; + set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null; + } + + public int Revision { get; set; } = 1; + + public double? KFactor { get; set; } + + private double? _defaultBendRadius; + public double? DefaultBendRadius + { + get => _defaultBendRadius; + set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null; + } + + public int BomItemId { get; set; } + public virtual BomItem BomItem { get; set; } + } +} diff --git a/FabWorks.Core/Models/ExportRecord.cs b/FabWorks.Core/Models/ExportRecord.cs new file mode 100644 index 0000000..29b5937 --- /dev/null +++ b/FabWorks.Core/Models/ExportRecord.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace FabWorks.Core.Models +{ + public class ExportRecord + { + public int Id { get; set; } + public string DrawingNumber { get; set; } + public string Title { get; set; } + public string EquipmentNo { get; set; } + public string DrawingNo { get; set; } + public string SourceFilePath { get; set; } + public string OutputFolder { get; set; } + public DateTime ExportedAt { get; set; } + public string ExportedBy { get; set; } + public string PdfContentHash { get; set; } + + public virtual ICollection BomItems { get; set; } = new List(); + } +} diff --git a/FabWorks.Core/Models/FormProgram.cs b/FabWorks.Core/Models/FormProgram.cs new file mode 100644 index 0000000..feaa03f --- /dev/null +++ b/FabWorks.Core/Models/FormProgram.cs @@ -0,0 +1,20 @@ +namespace FabWorks.Core.Models +{ + public class FormProgram + { + public int Id { get; set; } + public string ProgramFilePath { get; set; } = ""; + public string ContentHash { get; set; } + public string ProgramName { get; set; } = ""; + public double? Thickness { get; set; } + public string MaterialType { get; set; } = ""; + public double? KFactor { get; set; } + public int BendCount { get; set; } + public string UpperToolNames { get; set; } = ""; + public string LowerToolNames { get; set; } = ""; + public string SetupNotes { get; set; } = ""; + + public int BomItemId { get; set; } + public virtual BomItem BomItem { get; set; } + } +} diff --git a/FabWorks.Core/PressBrake/Extensions.cs b/FabWorks.Core/PressBrake/Extensions.cs new file mode 100644 index 0000000..d86ce26 --- /dev/null +++ b/FabWorks.Core/PressBrake/Extensions.cs @@ -0,0 +1,171 @@ +using System; +using System.Xml.Linq; + +namespace FabWorks.Core.PressBrake +{ + internal static class Extensions + { + private static bool? ToBool(this string s) + { + if (string.IsNullOrWhiteSpace(s)) + return null; + + int intValue; + + if (!int.TryParse(s, out intValue)) + return null; + + return Convert.ToBoolean(intValue); + } + + public static bool ToBool(this XAttribute a, bool defaultValue = false) + { + if (a == null) + return defaultValue; + + var b = a.Value.ToBool(); + + return b != null ? b.Value : defaultValue; + } + + public static bool? ToBoolOrNull(this XAttribute a) + { + if (a == null) + return null; + + return a.Value.ToBool(); + } + + private static int? ToInt(this string s) + { + if (string.IsNullOrWhiteSpace(s)) + return null; + + int intValue; + + if (!int.TryParse(s, out intValue)) + return null; + + return intValue; + } + + public static int ToInt(this XAttribute a, int defaultValue = 0) + { + if (a == null) + return defaultValue; + + var b = a.Value.ToInt(); + + return b != null ? b.Value : defaultValue; + } + + public static int? ToIntOrNull(this XAttribute a) + { + if (a == null) + return null; + + return a.Value.ToInt(); + } + + public static int ToInt(this XElement a, int defaultValue = 0) + { + if (a == null) + return defaultValue; + + var b = a.Value.ToInt(); + + return b != null ? b.Value : defaultValue; + } + + public static int? ToIntOrNull(this XElement a) + { + if (a == null) + return null; + + return a.Value.ToInt(); + } + + private static double? ToDouble(this string s) + { + if (string.IsNullOrWhiteSpace(s)) + return null; + + double d; + + if (!double.TryParse(s, out d)) + return null; + + return d; + } + + public static double ToDouble(this XAttribute a, double defaultValue = 0) + { + if (a == null) + return defaultValue; + + var b = a.Value.ToDouble(); + + return b != null ? b.Value : defaultValue; + } + + public static double? ToDoubleOrNull(this XAttribute a) + { + if (a == null) + return null; + + return a.Value.ToDouble(); + } + + public static double ToDouble(this XElement a, double defaultValue = 0) + { + if (a == null) + return defaultValue; + + var b = a.Value.ToDouble(); + + return b != null ? b.Value : defaultValue; + } + + public static double? ToDoubleOrNull(this XElement a) + { + if (a == null) + return null; + + return a.Value.ToDouble(); + } + + public static DateTime? ToDateTime(this XAttribute a) + { + if (a == null || string.IsNullOrWhiteSpace(a.Value)) + return null; + + DateTime d; + + if (!DateTime.TryParse(a.Value, out d)) + return null; + + return d; + } + + public static TimeSpan? ToTimeSpan(this XElement e) + { + if (e == null || string.IsNullOrWhiteSpace(e.Value)) + return null; + + TimeSpan d; + + if (!TimeSpan.TryParse(e.Value, out d)) + return null; + + return d; + } + + public static DateTime RoundDown(this DateTime dt, TimeSpan d) + { + var modTicks = dt.Ticks % d.Ticks; + var delta = -modTicks; + + return new DateTime(dt.Ticks + delta, dt.Kind); + } + } +} diff --git a/FabWorks.Core/PressBrake/MatType.cs b/FabWorks.Core/PressBrake/MatType.cs new file mode 100644 index 0000000..c2716f8 --- /dev/null +++ b/FabWorks.Core/PressBrake/MatType.cs @@ -0,0 +1,11 @@ +namespace FabWorks.Core.PressBrake +{ + public enum MatType + { + MildSteel, + HighStrengthSteel, + Stainless, + SoftAluminum, + HardAluminum + } +} diff --git a/FabWorks.Core/PressBrake/Program.cs b/FabWorks.Core/PressBrake/Program.cs new file mode 100644 index 0000000..7b36a87 --- /dev/null +++ b/FabWorks.Core/PressBrake/Program.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace FabWorks.Core.PressBrake +{ + public class Program + { + public Program() + { + UpperToolSets = new List(); + LowerToolSets = new List(); + Steps = new List(); + } + + public int Version { get; set; } + + public string ProgName { get; set; } + + public string FilePath { get; set; } + + public double MatThick { get; set; } + + public MatType MatType { get; set; } + + public double KFactor { get; set; } + + public string TeachName { get; set; } + + public string PartName { get; set; } + + public string SetupNotes { get; set; } + + public string ProgNotes { get; set; } + + public bool RZEnabled { get; set; } + + public List UpperToolSets { get; set; } + + public List LowerToolSets { get; set; } + + public List Steps { get; set; } + + public static Program Load(string file) + { + var reader = new ProgramReader(); + reader.Read(file); + return reader.Program; + } + + public static Program Load(Stream stream) + { + var reader = new ProgramReader(); + reader.Read(stream); + return reader.Program; + } + } +} diff --git a/FabWorks.Core/PressBrake/ProgramReader.cs b/FabWorks.Core/PressBrake/ProgramReader.cs new file mode 100644 index 0000000..0267e79 --- /dev/null +++ b/FabWorks.Core/PressBrake/ProgramReader.cs @@ -0,0 +1,149 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace FabWorks.Core.PressBrake +{ + public class ProgramReader + { + public Program Program { get; set; } + + public ProgramReader() + { + Program = new Program(); + } + + public void Read(string file) + { + var xml = XDocument.Load(file); + Program.FilePath = file; + Read(xml); + } + + public void Read(Stream stream) + { + var xml = XDocument.Load(stream); + Read(xml); + } + + private void Read(XDocument doc) + { + var data = doc.Root.Element("PressBrakeProgram"); + + Program.Version = data.Attribute("Version").ToInt(); + Program.ProgName = data.Attribute("ProgName")?.Value; + Program.MatThick = data.Attribute("MatThick").ToDouble(); + Program.MatType = GetMaterialType(data.Attribute("MatType")?.Value); + Program.KFactor = data.Attribute("KFactor").ToDouble(); + Program.TeachName = data.Attribute("TeachName")?.Value; + Program.PartName = data.Attribute("PartName")?.Value; + Program.SetupNotes = data.Attribute("SetupNotes")?.Value; + Program.ProgNotes = data.Attribute("ProgNotes")?.Value; + Program.RZEnabled = Convert.ToBoolean(data.Attribute("RZEnabled").ToInt()); + + foreach (var item in data.Element("UpperToolSets").Descendants("ToolSetup")) + { + var setup = ReadToolSetup(item); + Program.UpperToolSets.Add(setup); + } + + foreach (var item in data.Element("LowerToolSets").Descendants("ToolSetup")) + { + var setup = ReadToolSetup(item); + Program.LowerToolSets.Add(setup); + } + + foreach (var item in data.Element("StepData").Descendants("Step")) + { + var step = ReadStep(item); + step.UpperTool = Program.UpperToolSets.FirstOrDefault(t => t.Id == step.UpperID); + step.LowerTool = Program.LowerToolSets.FirstOrDefault(t => t.Id == step.LowerID); + + Program.Steps.Add(step); + } + } + + private ToolSetup ReadToolSetup(XElement x) + { + var setup = new ToolSetup(); + + setup.Name = x.Attribute("Name").Value; + setup.Id = x.Attribute("ID").ToInt(); + setup.Length = x.Attribute("Length").ToDouble(); + setup.StackedHolderType = x.Attribute("StackedHolderType").ToInt(); + setup.HolderHeight = x.Attribute("HolderHeight").ToDouble(); + + foreach (var item in x.Descendants("SegEntry")) + { + var entry = new SegEntry(); + entry.SegValue = item.Attribute("SegValue").ToDouble(); + setup.Segments.Add(entry); + } + + return setup; + } + + private Step ReadStep(XElement x) + { + var step = new Step(); + + step.RevMode = x.Attribute("RevMode").ToInt(); + step.RevTons = x.Attribute("RevTons").ToDouble(); + step.MaxTons = x.Attribute("MaxTons").ToDouble(); + step.RevAbsPos = x.Attribute("RevAbsPos").ToDouble(); + step.ActualAng = x.Attribute("ActualAng").ToDouble(); + step.AngleAdj = x.Attribute("AngleAdj").ToDouble(); + step.BendLen = x.Attribute("BendLen").ToDouble(); + step.StrokeLen = x.Attribute("StrokeLen").ToDouble(); + step.UpperID = x.Attribute("UpperID").ToInt(); + step.LowerID = x.Attribute("LowerID").ToInt(); + step.SpdChgDwn = x.Attribute("SpdChgDwn").ToDouble(); + step.SpdChgUp = x.Attribute("SpdChgUp").ToDouble(); + step.Tilt = x.Attribute("Tilt").ToDouble(); + step.FormSpeed = x.Attribute("FormSpeed").ToDouble(); + step.XLeft = x.Attribute("XLeft").ToDouble(); + step.XRight = x.Attribute("XRight").ToDouble(); + step.RLeft = x.Attribute("RLeft").ToDouble(); + step.RRight = x.Attribute("RRight").ToDouble(); + step.ZLeft = x.Attribute("ZLeft").ToDouble(); + step.ZRight = x.Attribute("ZRight").ToDouble(); + step.FLeft = x.Attribute("FLeft").ToDouble(); + step.FRight = x.Attribute("FRight").ToDouble(); + step.SSLeft = x.Attribute("SSLeft").ToDouble(); + step.SSRight = x.Attribute("SSRight").ToDouble(); + step.ReturnSpd = x.Attribute("ReturnSpd").ToDouble(); + step.SideFlgHeight = x.Attribute("SideFlgHeight").ToDouble(); + + return step; + } + + private MatType GetMaterialType(string value) + { + if (value == null) + return MatType.MildSteel; + + int i; + + if (!int.TryParse(value, out i)) + return MatType.MildSteel; + + switch (i) + { + case 0: + return MatType.MildSteel; + case 1: + return MatType.HighStrengthSteel; + case 2: + return MatType.Stainless; + case 3: + return MatType.SoftAluminum; + case 4: + return MatType.HardAluminum; + } + + return MatType.MildSteel; + } + } +} diff --git a/FabWorks.Core/PressBrake/SegEntry.cs b/FabWorks.Core/PressBrake/SegEntry.cs new file mode 100644 index 0000000..e6ed33d --- /dev/null +++ b/FabWorks.Core/PressBrake/SegEntry.cs @@ -0,0 +1,7 @@ +namespace FabWorks.Core.PressBrake +{ + public class SegEntry + { + public double SegValue { get; set; } + } +} diff --git a/FabWorks.Core/PressBrake/Step.cs b/FabWorks.Core/PressBrake/Step.cs new file mode 100644 index 0000000..3520a17 --- /dev/null +++ b/FabWorks.Core/PressBrake/Step.cs @@ -0,0 +1,36 @@ +namespace FabWorks.Core.PressBrake +{ + public class Step + { + public int RevMode { get; set; } + public double RevTons { get; set; } + public double MaxTons { get; set; } + public double RevAbsPos { get; set; } + public double ActualAng { get; set; } + public double AngleAdj { get; set; } + public double BendLen { get; set; } + public double StrokeLen { get; set; } + public double Tilt { get; set; } + public int UpperID { get; set; } + public int LowerID { get; set; } + public double SpdChgDwn { get; set; } + public double SpdChgUp { get; set; } + public double FormSpeed { get; set; } + public double XLeft { get; set; } + public double XRight { get; set; } + public double RLeft { get; set; } + public double RRight { get; set; } + public double ZLeft { get; set; } + public double ZRight { get; set; } + public double FLeft { get; set; } + public double FRight { get; set; } + + public double SSLeft { get; set; } + public double SSRight { get; set; } + public double ReturnSpd { get; set; } + public double SideFlgHeight { get; set; } + + public ToolSetup UpperTool { get; set; } + public ToolSetup LowerTool { get; set; } + } +} diff --git a/FabWorks.Core/PressBrake/ToolSetup.cs b/FabWorks.Core/PressBrake/ToolSetup.cs new file mode 100644 index 0000000..c71d4b9 --- /dev/null +++ b/FabWorks.Core/PressBrake/ToolSetup.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace FabWorks.Core.PressBrake +{ + public class ToolSetup + { + public ToolSetup() + { + Segments = new List(); + } + + public string Name { get; set; } + + public int Id { get; set; } + + public double Length { get; set; } + + public int StackedHolderType { get; set; } + + public double HolderHeight { get; set; } + + public List Segments { get; set; } + } +} diff --git a/FabWorks.Tests/FabWorks.Tests.csproj b/FabWorks.Tests/FabWorks.Tests.csproj new file mode 100644 index 0000000..12e226c --- /dev/null +++ b/FabWorks.Tests/FabWorks.Tests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FabWorks.Tests/FormProgramServiceTests.cs b/FabWorks.Tests/FormProgramServiceTests.cs new file mode 100644 index 0000000..146355c --- /dev/null +++ b/FabWorks.Tests/FormProgramServiceTests.cs @@ -0,0 +1,53 @@ +using FabWorks.Api.Services; +using Xunit; + +namespace FabWorks.Tests +{ + public class FormProgramServiceTests + { + [Fact] + public void ParseFromFile_SamplePgm_PopulatesMaterialType() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + + // ProgName is empty in the sample file, so verify MaterialType instead + Assert.False(string.IsNullOrEmpty(fp.MaterialType)); + } + + [Fact] + public void ParseFromFile_SamplePgm_PopulatesThickness() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.NotNull(fp.Thickness); + Assert.True(fp.Thickness > 0); + } + + [Fact] + public void ParseFromFile_SamplePgm_PopulatesBendCount() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.True(fp.BendCount > 0); + } + + [Fact] + public void ParseFromFile_SamplePgm_PopulatesToolNames() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.False(string.IsNullOrEmpty(fp.UpperToolNames)); + Assert.False(string.IsNullOrEmpty(fp.LowerToolNames)); + } + + [Fact] + public void ParseFromFile_SamplePgm_ComputesContentHash() + { + var service = new FormProgramService(); + var fp = service.ParseFromFile("TestData/sample.pgm"); + Assert.NotNull(fp.ContentHash); + Assert.Equal(64, fp.ContentHash.Length); // SHA256 hex = 64 chars + } + } +} diff --git a/FabWorks.Tests/PressBrake/ProgramReaderTests.cs b/FabWorks.Tests/PressBrake/ProgramReaderTests.cs new file mode 100644 index 0000000..b2614cf --- /dev/null +++ b/FabWorks.Tests/PressBrake/ProgramReaderTests.cs @@ -0,0 +1,48 @@ +using FabWorks.Core.PressBrake; +using Xunit; + +namespace FabWorks.Tests.PressBrake +{ + public class ProgramReaderTests + { + [Fact] + public void Load_SamplePgm_ParsesProgramAttributes() + { + var pgm = Program.Load("TestData/sample.pgm"); + + // ProgName may be empty on some exports; verify PartName was parsed instead + Assert.False(string.IsNullOrEmpty(pgm.PartName)); + } + + [Fact] + public void Load_SamplePgm_ParsesThickness() + { + var pgm = Program.Load("TestData/sample.pgm"); + Assert.True(pgm.MatThick > 0); + } + + [Fact] + public void Load_SamplePgm_ParsesSteps() + { + var pgm = Program.Load("TestData/sample.pgm"); + Assert.NotEmpty(pgm.Steps); + } + + [Fact] + public void Load_SamplePgm_ParsesToolSetups() + { + var pgm = Program.Load("TestData/sample.pgm"); + Assert.NotEmpty(pgm.UpperToolSets); + Assert.NotEmpty(pgm.LowerToolSets); + } + + [Fact] + public void Load_SamplePgm_ResolvesStepToolReferences() + { + var pgm = Program.Load("TestData/sample.pgm"); + var step = pgm.Steps[0]; + Assert.NotNull(step.UpperTool); + Assert.NotNull(step.LowerTool); + } + } +} diff --git a/FabWorks.Tests/TestData/sample.pgm b/FabWorks.Tests/TestData/sample.pgm new file mode 100644 index 0000000..76b06ad --- /dev/null +++ b/FabWorks.Tests/TestData/sample.pgm @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +