Compare commits
13 Commits
master
...
feature/fa
| Author | SHA1 | Date | |
|---|---|---|---|
| bd3e7c2a36 | |||
| b9e84de7c0 | |||
| f6cd91f1b5 | |||
| 3554bb6110 | |||
| 4707e96359 | |||
| c5bd7fb4c8 | |||
| 13c61a82a4 | |||
| 444a077cbc | |||
| c4920f933d | |||
| b472729fda | |||
| 5d2948d563 | |||
| 71c65e0bf5 | |||
| 53aa23f762 |
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -246,5 +246,5 @@ ModelManifest.xml
|
||||
# Test documents
|
||||
TestDocs/
|
||||
|
||||
# Superpowers specs and plans
|
||||
docs/superpowers/
|
||||
# Claude Code local settings
|
||||
.claude/
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29123.88
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.3.11512.155 d18.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportDXF", "ExportDXF\ExportDXF.csproj", "{05F21D73-FD31-4E77-8D9B-41C86D4D8305}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLines\EtchBendLines\EtchBendLines.csproj", "{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
|
||||
@@ -41,6 +47,42 @@ Global
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.Build.0 = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.Build.0 = Release|Any CPU
|
||||
{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
|
||||
|
||||
157
ExportDXF/ApiClient/FabWorksApiClient.cs
Normal file
157
ExportDXF/ApiClient/FabWorksApiClient.cs
Normal file
@@ -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<ApiExportDetail> 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<ApiExportDetail>();
|
||||
}
|
||||
|
||||
public async Task<ApiExportDetail> 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<ApiExportDetail>();
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetDrawingNumbersAsync()
|
||||
{
|
||||
var response = await _http.GetAsync("api/exports/drawing-numbers");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<string>>();
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetEquipmentNumbersAsync()
|
||||
{
|
||||
var response = await _http.GetAsync("api/exports/equipment-numbers");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<string>>();
|
||||
}
|
||||
|
||||
public async Task<List<string>> 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<List<string>>();
|
||||
}
|
||||
|
||||
public async Task<string> 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<string> 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<ApiBomItem> 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<ApiBomItem>();
|
||||
}
|
||||
|
||||
public async Task<ApiBomItem> CreateBomItemAsync(int exportId, ApiBomItem bomItem)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"api/exports/{exportId}/bom-items", bomItem);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiBomItem>();
|
||||
}
|
||||
|
||||
public async Task<ApiCutTemplate> 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<ApiCutTemplate>();
|
||||
}
|
||||
|
||||
public async Task<ApiFileUploadResponse> 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<ApiFileUploadResponse>();
|
||||
}
|
||||
|
||||
public async Task<ApiFileUploadResponse> 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<ApiFileUploadResponse>();
|
||||
}
|
||||
}
|
||||
}
|
||||
86
ExportDXF/ApiClient/FabWorksApiDtos.cs
Normal file
86
ExportDXF/ApiClient/FabWorksApiDtos.cs
Normal file
@@ -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<ApiBomItem> 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; }
|
||||
}
|
||||
}
|
||||
22
ExportDXF/ApiClient/IFabWorksApiClient.cs
Normal file
22
ExportDXF/ApiClient/IFabWorksApiClient.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ExportDXF.ApiClient
|
||||
{
|
||||
public interface IFabWorksApiClient
|
||||
{
|
||||
Task<ApiExportDetail> CreateExportAsync(string drawingNumber, string equipmentNo, string drawingNo, string sourceFilePath, string outputFolder, string title = null);
|
||||
Task<ApiExportDetail> GetExportBySourceFileAsync(string filePath);
|
||||
Task<List<string>> GetDrawingNumbersAsync();
|
||||
Task<List<string>> GetEquipmentNumbersAsync();
|
||||
Task<List<string>> GetDrawingNumbersByEquipmentAsync(string equipmentNo = null);
|
||||
Task<string> GetNextItemNumberAsync(string drawingNumber);
|
||||
Task UpdatePdfHashAsync(int exportId, string pdfContentHash);
|
||||
Task<string> GetPreviousPdfHashAsync(string drawingNumber, int? excludeId = null);
|
||||
Task<ApiBomItem> FindExistingBomItemAsync(int exportId, string partName, string configurationName);
|
||||
Task<ApiBomItem> CreateBomItemAsync(int exportId, ApiBomItem bomItem);
|
||||
Task<ApiCutTemplate> GetPreviousCutTemplateAsync(string drawingNumber, string itemNo);
|
||||
Task<ApiFileUploadResponse> UploadDxfAsync(string localFilePath, string equipment, string drawingNo, string itemNo, string contentHash);
|
||||
Task<ApiFileUploadResponse> UploadPdfAsync(string localFilePath, string equipment, string drawingNo, string contentHash, int? exportRecordId = null);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ExportDXF
|
||||
@@ -13,6 +14,35 @@ namespace ExportDXF
|
||||
|
||||
public string Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The descriptive text after the equipment/drawing number (e.g. "Prox switch bracket for drive").
|
||||
/// </summary>
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Source) || string.IsNullOrEmpty(EquipmentNo))
|
||||
return null;
|
||||
|
||||
// Strip equipment number (and optional drawing number) from the source to get the description
|
||||
var prefix = string.IsNullOrEmpty(DrawingNo)
|
||||
? EquipmentNo
|
||||
: EquipmentNo + " " + DrawingNo;
|
||||
|
||||
var desc = Source;
|
||||
if (desc.StartsWith(prefix, System.StringComparison.OrdinalIgnoreCase))
|
||||
desc = desc.Substring(prefix.Length);
|
||||
|
||||
// Remove file extension (e.g. ".SLDPRT")
|
||||
var ext = Path.GetExtension(desc);
|
||||
if (!string.IsNullOrEmpty(ext))
|
||||
desc = desc.Substring(0, desc.Length - ext.Length);
|
||||
|
||||
desc = desc.Trim();
|
||||
return string.IsNullOrEmpty(desc) ? null : desc;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(DrawingNo))
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.104.2" />
|
||||
<PackageReference Include="PDFtoImage" Version="4.1.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
116
ExportDXF/Forms/MainForm.Designer.cs
generated
116
ExportDXF/Forms/MainForm.Designer.cs
generated
@@ -38,8 +38,12 @@ namespace ExportDXF.Forms
|
||||
bomDataGrid = new System.Windows.Forms.DataGridView();
|
||||
cutTemplatesTab = new System.Windows.Forms.TabPage();
|
||||
cutTemplatesDataGrid = new System.Windows.Forms.DataGridView();
|
||||
templateLabel = new System.Windows.Forms.Label();
|
||||
txtFilenameTemplate = new System.Windows.Forms.TextBox();
|
||||
equipmentBox = new System.Windows.Forms.ComboBox();
|
||||
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();
|
||||
@@ -55,32 +59,16 @@ namespace ExportDXF.Forms
|
||||
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(65, 57);
|
||||
runButton.Size = new System.Drawing.Size(65, 87);
|
||||
runButton.TabIndex = 11;
|
||||
runButton.Text = "Start";
|
||||
runButton.UseVisualStyleBackColor = true;
|
||||
runButton.Click += button1_Click;
|
||||
//
|
||||
// templateLabel
|
||||
//
|
||||
templateLabel.AutoSize = true;
|
||||
templateLabel.Location = new System.Drawing.Point(15, 15);
|
||||
templateLabel.Name = "templateLabel";
|
||||
templateLabel.Size = new System.Drawing.Size(116, 17);
|
||||
templateLabel.TabIndex = 2;
|
||||
templateLabel.Text = "Filename Template";
|
||||
//
|
||||
// txtFilenameTemplate
|
||||
//
|
||||
txtFilenameTemplate.Location = new System.Drawing.Point(137, 12);
|
||||
txtFilenameTemplate.Name = "txtFilenameTemplate";
|
||||
txtFilenameTemplate.Size = new System.Drawing.Size(365, 25);
|
||||
txtFilenameTemplate.TabIndex = 1;
|
||||
//
|
||||
// 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;
|
||||
@@ -90,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;
|
||||
@@ -101,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, 75);
|
||||
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(910, 522);
|
||||
mainTabControl.Size = new System.Drawing.Size(910, 492);
|
||||
mainTabControl.TabIndex = 12;
|
||||
//
|
||||
// logEventsTab
|
||||
@@ -114,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(902, 488);
|
||||
logEventsTab.Size = new System.Drawing.Size(902, 458);
|
||||
logEventsTab.TabIndex = 0;
|
||||
logEventsTab.Text = "Log Events";
|
||||
logEventsTab.UseVisualStyleBackColor = true;
|
||||
@@ -126,16 +114,16 @@ namespace ExportDXF.Forms
|
||||
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
logEventsDataGrid.Name = "logEventsDataGrid";
|
||||
logEventsDataGrid.Size = new System.Drawing.Size(890, 476);
|
||||
logEventsDataGrid.Size = new System.Drawing.Size(890, 444);
|
||||
logEventsDataGrid.TabIndex = 0;
|
||||
//
|
||||
// bomTab
|
||||
//
|
||||
bomTab.Controls.Add(bomDataGrid);
|
||||
bomTab.Location = new System.Drawing.Point(4, 28);
|
||||
bomTab.Location = new System.Drawing.Point(4, 30);
|
||||
bomTab.Name = "bomTab";
|
||||
bomTab.Padding = new System.Windows.Forms.Padding(3);
|
||||
bomTab.Size = new System.Drawing.Size(902, 490);
|
||||
bomTab.Size = new System.Drawing.Size(902, 458);
|
||||
bomTab.TabIndex = 1;
|
||||
bomTab.Text = "Bill Of Materials";
|
||||
bomTab.UseVisualStyleBackColor = true;
|
||||
@@ -147,16 +135,16 @@ namespace ExportDXF.Forms
|
||||
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
bomDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
bomDataGrid.Name = "bomDataGrid";
|
||||
bomDataGrid.Size = new System.Drawing.Size(890, 478);
|
||||
bomDataGrid.Size = new System.Drawing.Size(890, 444);
|
||||
bomDataGrid.TabIndex = 1;
|
||||
//
|
||||
// cutTemplatesTab
|
||||
//
|
||||
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
|
||||
cutTemplatesTab.Location = new System.Drawing.Point(4, 28);
|
||||
cutTemplatesTab.Location = new System.Drawing.Point(4, 30);
|
||||
cutTemplatesTab.Name = "cutTemplatesTab";
|
||||
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
|
||||
cutTemplatesTab.Size = new System.Drawing.Size(902, 490);
|
||||
cutTemplatesTab.Size = new System.Drawing.Size(902, 458);
|
||||
cutTemplatesTab.TabIndex = 2;
|
||||
cutTemplatesTab.Text = "Cut Templates";
|
||||
cutTemplatesTab.UseVisualStyleBackColor = true;
|
||||
@@ -168,17 +156,71 @@ namespace ExportDXF.Forms
|
||||
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
|
||||
cutTemplatesDataGrid.Size = new System.Drawing.Size(890, 478);
|
||||
cutTemplatesDataGrid.Size = new System.Drawing.Size(890, 447);
|
||||
cutTemplatesDataGrid.TabIndex = 2;
|
||||
//
|
||||
// equipmentBox
|
||||
//
|
||||
equipmentBox.FormattingEnabled = true;
|
||||
equipmentBox.Location = new System.Drawing.Point(137, 12);
|
||||
equipmentBox.Name = "equipmentBox";
|
||||
equipmentBox.Size = new System.Drawing.Size(166, 25);
|
||||
equipmentBox.TabIndex = 13;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new System.Drawing.Point(61, 15);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(70, 17);
|
||||
label1.TabIndex = 2;
|
||||
label1.Text = "Equipment";
|
||||
//
|
||||
// label2
|
||||
//
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new System.Drawing.Point(321, 15);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(56, 17);
|
||||
label2.TabIndex = 2;
|
||||
label2.Text = "Drawing";
|
||||
//
|
||||
// drawingNoBox
|
||||
//
|
||||
drawingNoBox.FormattingEnabled = true;
|
||||
drawingNoBox.Location = new System.Drawing.Point(383, 12);
|
||||
drawingNoBox.Name = "drawingNoBox";
|
||||
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(937, 609);
|
||||
Controls.Add(txtFilenameTemplate);
|
||||
Controls.Add(titleBox);
|
||||
Controls.Add(titleLabel);
|
||||
Controls.Add(drawingNoBox);
|
||||
Controls.Add(equipmentBox);
|
||||
Controls.Add(mainTabControl);
|
||||
Controls.Add(viewFlipDeciderBox);
|
||||
Controls.Add(templateLabel);
|
||||
Controls.Add(label2);
|
||||
Controls.Add(label1);
|
||||
Controls.Add(label3);
|
||||
Controls.Add(runButton);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
|
||||
@@ -211,7 +253,11 @@ namespace ExportDXF.Forms
|
||||
private System.Windows.Forms.DataGridView bomDataGrid;
|
||||
private System.Windows.Forms.TabPage cutTemplatesTab;
|
||||
private System.Windows.Forms.DataGridView cutTemplatesDataGrid;
|
||||
private System.Windows.Forms.Label templateLabel;
|
||||
private System.Windows.Forms.TextBox txtFilenameTemplate;
|
||||
private System.Windows.Forms.ComboBox equipmentBox;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using ExportDXF.ApiClient;
|
||||
using ExportDXF.Extensions;
|
||||
using ExportDXF.Models;
|
||||
using ExportDXF.Services;
|
||||
using ExportDXF.ViewFlipDeciders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -17,13 +18,14 @@ namespace ExportDXF.Forms
|
||||
{
|
||||
private readonly ISolidWorksService _solidWorksService;
|
||||
private readonly IDxfExportService _exportService;
|
||||
private readonly IDrawingInfoExtractor[] _extractors;
|
||||
private readonly IFabWorksApiClient _apiClient;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly BindingList<LogEvent> _logEvents;
|
||||
private readonly BindingList<BomItem> _bomItems;
|
||||
private readonly BindingList<CutTemplate> _cutTemplates;
|
||||
private List<DrawingInfo> _allDrawings;
|
||||
|
||||
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IDrawingInfoExtractor[] extractors)
|
||||
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFabWorksApiClient apiClient)
|
||||
{
|
||||
InitializeComponent();
|
||||
_solidWorksService = solidWorksService ??
|
||||
@@ -31,15 +33,17 @@ namespace ExportDXF.Forms
|
||||
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
|
||||
_exportService = exportService ??
|
||||
throw new ArgumentNullException(nameof(exportService));
|
||||
_extractors = extractors ??
|
||||
throw new ArgumentNullException(nameof(extractors));
|
||||
_apiClient = apiClient ??
|
||||
throw new ArgumentNullException(nameof(apiClient));
|
||||
_logEvents = new BindingList<LogEvent>();
|
||||
_bomItems = new BindingList<BomItem>();
|
||||
_cutTemplates = new BindingList<CutTemplate>();
|
||||
_allDrawings = new List<DrawingInfo>();
|
||||
InitializeViewFlipDeciders();
|
||||
InitializeLogEventsGrid();
|
||||
InitializeBomGrid();
|
||||
InitializeCutTemplatesGrid();
|
||||
InitializeDrawingDropdowns();
|
||||
}
|
||||
|
||||
~MainForm()
|
||||
@@ -64,8 +68,10 @@ namespace ExportDXF.Forms
|
||||
LogMessage("Connecting to SolidWorks, this may take a minute...");
|
||||
await _solidWorksService.ConnectAsync();
|
||||
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
|
||||
LogMessage("Files will be uploaded to FabWorks API");
|
||||
await LoadDrawingDropdownsAsync();
|
||||
LogMessage("Ready");
|
||||
UpdateActiveDocumentDisplay();
|
||||
await UpdateActiveDocumentDisplayAsync();
|
||||
runButton.Enabled = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -85,7 +91,7 @@ namespace ExportDXF.Forms
|
||||
ViewFlipDecider = d
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Move "Automatic" to the top if it exists
|
||||
var automatic = items.FirstOrDefault(i => i.Name == "Automatic");
|
||||
if (automatic != null)
|
||||
{
|
||||
@@ -98,13 +104,17 @@ namespace ExportDXF.Forms
|
||||
|
||||
private void InitializeLogEventsGrid()
|
||||
{
|
||||
// Clear any existing columns first
|
||||
logEventsDataGrid.Columns.Clear();
|
||||
|
||||
// Configure grid settings
|
||||
logEventsDataGrid.AutoGenerateColumns = false;
|
||||
logEventsDataGrid.AllowUserToAddRows = false;
|
||||
logEventsDataGrid.AllowUserToDeleteRows = false;
|
||||
logEventsDataGrid.ReadOnly = true;
|
||||
logEventsDataGrid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||||
|
||||
// Add columns
|
||||
logEventsDataGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = nameof(LogEvent.Time),
|
||||
@@ -134,19 +144,26 @@ namespace ExportDXF.Forms
|
||||
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
|
||||
});
|
||||
|
||||
// Add row coloring based on log level
|
||||
logEventsDataGrid.CellFormatting += LogEventsDataGrid_CellFormatting;
|
||||
|
||||
// Set the data source AFTER adding columns
|
||||
logEventsDataGrid.DataSource = _logEvents;
|
||||
}
|
||||
|
||||
private void InitializeBomGrid()
|
||||
{
|
||||
// Clear any existing columns first
|
||||
bomDataGrid.Columns.Clear();
|
||||
|
||||
// Configure grid settings
|
||||
bomDataGrid.AutoGenerateColumns = false;
|
||||
bomDataGrid.AllowUserToAddRows = false;
|
||||
bomDataGrid.AllowUserToDeleteRows = false;
|
||||
bomDataGrid.ReadOnly = true;
|
||||
bomDataGrid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||||
|
||||
// Add columns
|
||||
bomDataGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = nameof(BomItem.ItemNo),
|
||||
@@ -196,12 +213,14 @@ namespace ExportDXF.Forms
|
||||
Width = 120
|
||||
});
|
||||
|
||||
// Set the data source AFTER adding columns
|
||||
bomDataGrid.DataSource = _bomItems;
|
||||
}
|
||||
|
||||
private void InitializeCutTemplatesGrid()
|
||||
{
|
||||
cutTemplatesDataGrid.Columns.Clear();
|
||||
|
||||
cutTemplatesDataGrid.AutoGenerateColumns = false;
|
||||
cutTemplatesDataGrid.AllowUserToAddRows = false;
|
||||
cutTemplatesDataGrid.AllowUserToDeleteRows = false;
|
||||
@@ -222,13 +241,6 @@ namespace ExportDXF.Forms
|
||||
Width = 250
|
||||
});
|
||||
|
||||
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = nameof(CutTemplate.Revision),
|
||||
HeaderText = "Rev",
|
||||
Width = 50
|
||||
});
|
||||
|
||||
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = nameof(CutTemplate.Thickness),
|
||||
@@ -260,6 +272,69 @@ namespace ExportDXF.Forms
|
||||
cutTemplatesDataGrid.DataSource = _cutTemplates;
|
||||
}
|
||||
|
||||
private void InitializeDrawingDropdowns()
|
||||
{
|
||||
// Wire up event handler; actual data loading happens in LoadDrawingDropdownsAsync
|
||||
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
|
||||
}
|
||||
|
||||
private async Task LoadDrawingDropdownsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var equipmentNumbers = await _apiClient.GetEquipmentNumbersAsync();
|
||||
|
||||
equipmentBox.Items.Clear();
|
||||
equipmentBox.Items.Add("");
|
||||
foreach (var eq in equipmentNumbers)
|
||||
{
|
||||
equipmentBox.Items.Add(eq);
|
||||
}
|
||||
|
||||
// Clear _allDrawings — drawing list is now loaded on equipment selection
|
||||
_allDrawings = new List<DrawingInfo>();
|
||||
await UpdateDrawingDropdownAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// API might not be available yet - that's OK
|
||||
System.Diagnostics.Debug.WriteLine($"Failed to load equipment numbers from API: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
await UpdateDrawingDropdownAsync();
|
||||
}
|
||||
|
||||
private async Task UpdateDrawingDropdownAsync()
|
||||
{
|
||||
var selectedEquipment = equipmentBox.SelectedItem?.ToString();
|
||||
|
||||
drawingNoBox.Items.Clear();
|
||||
drawingNoBox.Items.Add("");
|
||||
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
drawingNoBox.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_cancellationTokenSource != null)
|
||||
@@ -276,13 +351,6 @@ namespace ExportDXF.Forms
|
||||
{
|
||||
try
|
||||
{
|
||||
var template = txtFilenameTemplate.Text.Trim();
|
||||
if (!FilenameTemplateParser.Validate(template, out var error))
|
||||
{
|
||||
MessageBox.Show(error, "Invalid Template", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
var token = _cancellationTokenSource.Token;
|
||||
UpdateUIForExportStart();
|
||||
@@ -294,27 +362,36 @@ namespace ExportDXF.Forms
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceDir = Path.GetDirectoryName(activeDoc.FilePath);
|
||||
var outputFolder = Path.Combine(sourceDir, "Templates");
|
||||
// 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}" : equipment)
|
||||
: activeDoc.Title;
|
||||
|
||||
var viewFlipDecider = GetSelectedViewFlipDecider();
|
||||
var title = titleBox.Text?.Trim();
|
||||
|
||||
var exportContext = new ExportContext
|
||||
{
|
||||
ActiveDocument = activeDoc,
|
||||
ViewFlipDecider = viewFlipDecider,
|
||||
FilenameTemplate = template,
|
||||
OutputFolder = outputFolder,
|
||||
FilePrefix = filePrefix,
|
||||
Equipment = equipment,
|
||||
DrawingNo = drawingNo,
|
||||
Title = string.IsNullOrEmpty(title) ? null : title,
|
||||
EquipmentId = null,
|
||||
CancellationToken = token,
|
||||
ProgressCallback = (msg, level, file) => LogMessage(msg, level, file),
|
||||
BomItemCallback = AddBomItem
|
||||
};
|
||||
|
||||
// Clear previous BOM items and cut templates
|
||||
_bomItems.Clear();
|
||||
_cutTemplates.Clear();
|
||||
|
||||
LogMessage($"Started at {DateTime.Now:t}");
|
||||
LogMessage($"Output: {outputFolder}");
|
||||
LogMessage("Exporting (files will be uploaded to API)...");
|
||||
|
||||
_solidWorksService.SetCommandInProgress(true);
|
||||
|
||||
@@ -355,29 +432,27 @@ namespace ExportDXF.Forms
|
||||
private void UpdateUIForExportStart()
|
||||
{
|
||||
viewFlipDeciderBox.Enabled = false;
|
||||
txtFilenameTemplate.Enabled = false;
|
||||
runButton.Text = "Stop";
|
||||
}
|
||||
|
||||
private void UpdateUIForExportComplete()
|
||||
{
|
||||
viewFlipDeciderBox.Enabled = true;
|
||||
txtFilenameTemplate.Enabled = true;
|
||||
runButton.Text = "Start";
|
||||
runButton.Enabled = true;
|
||||
}
|
||||
|
||||
private void OnActiveDocumentChanged(object sender, EventArgs e)
|
||||
private async void OnActiveDocumentChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action(() => UpdateActiveDocumentDisplay()));
|
||||
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";
|
||||
@@ -386,17 +461,69 @@ namespace ExportDXF.Forms
|
||||
if (activeDoc == null)
|
||||
return;
|
||||
|
||||
// Try each extractor to auto-fill the template
|
||||
foreach (var extractor in _extractors)
|
||||
// Try API first: look up the most recent export for this file path
|
||||
DrawingInfo drawingInfo = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(activeDoc.FilePath))
|
||||
{
|
||||
if (extractor.TryExtract(activeDoc.Title, out var info))
|
||||
drawingInfo = await LookupDrawingInfoFromHistoryAsync(activeDoc.FilePath);
|
||||
}
|
||||
|
||||
// Fall back to parsing the document title
|
||||
if (drawingInfo == null)
|
||||
{
|
||||
txtFilenameTemplate.Text = info.DefaultTemplate;
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(titleBox.Text) && !string.IsNullOrEmpty(drawingInfo.Description))
|
||||
titleBox.Text = drawingInfo.Description;
|
||||
|
||||
// Load drawings for the selected equipment, then set drawing number
|
||||
await UpdateDrawingDropdownAsync();
|
||||
|
||||
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
|
||||
|
||||
if (!string.IsNullOrEmpty(drawingInfo.DrawingNo))
|
||||
{
|
||||
if (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo))
|
||||
drawingNoBox.Items.Add(drawingInfo.DrawingNo);
|
||||
drawingNoBox.Text = drawingInfo.DrawingNo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DrawingInfo> 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);
|
||||
@@ -426,6 +553,7 @@ namespace ExportDXF.Forms
|
||||
|
||||
_logEvents.Add(logEvent);
|
||||
|
||||
// Auto-scroll to the last row
|
||||
if (logEventsDataGrid.Rows.Count > 0)
|
||||
{
|
||||
logEventsDataGrid.FirstDisplayedScrollingRowIndex = logEventsDataGrid.Rows.Count - 1;
|
||||
|
||||
@@ -2,6 +2,7 @@ namespace ExportDXF.Models
|
||||
{
|
||||
public class BomItem
|
||||
{
|
||||
public int ID { get; set; }
|
||||
public string ItemNo { get; set; } = "";
|
||||
public string PartNo { get; set; } = "";
|
||||
public int SortOrder { get; set; }
|
||||
@@ -11,7 +12,13 @@ namespace ExportDXF.Models
|
||||
public string PartName { get; set; } = "";
|
||||
public string ConfigurationName { get; set; } = "";
|
||||
public string Material { get; set; } = "";
|
||||
public CutTemplate CutTemplate { get; set; }
|
||||
|
||||
// EF Core relationship to ExportRecord
|
||||
public int ExportRecordId { get; set; }
|
||||
public virtual ExportRecord ExportRecord { get; set; }
|
||||
|
||||
// Optional 1:1 relationship to CutTemplate (only for sheet metal parts)
|
||||
public virtual CutTemplate CutTemplate { get; set; }
|
||||
}
|
||||
|
||||
public struct Size
|
||||
|
||||
@@ -4,11 +4,12 @@ namespace ExportDXF.Models
|
||||
{
|
||||
public class CutTemplate
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string DxfFilePath { get; set; } = "";
|
||||
public string ContentHash { get; set; }
|
||||
public string CutTemplateName { get; set; } = "";
|
||||
public int Revision { get; set; } = 1;
|
||||
|
||||
// Sheet metal properties (moved from BomItem)
|
||||
private double? _thickness;
|
||||
public double? Thickness
|
||||
{
|
||||
@@ -24,5 +25,9 @@ namespace ExportDXF.Models
|
||||
get => _defaultBendRadius;
|
||||
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
|
||||
}
|
||||
|
||||
// FK back to BomItem
|
||||
public int BomItemId { get; set; }
|
||||
public virtual BomItem BomItem { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using ExportDXF.Models;
|
||||
using ExportDXF.Models;
|
||||
using ExportDXF.ViewFlipDeciders;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
using SolidWorks.Interop.swconst;
|
||||
@@ -28,14 +28,29 @@ namespace ExportDXF.Services
|
||||
public IViewFlipDecider ViewFlipDecider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filename template with placeholders (e.g., "4321 A01 PT{item_no:2}").
|
||||
/// Prefix to prepend to exported filenames.
|
||||
/// </summary>
|
||||
public string FilenameTemplate { get; set; }
|
||||
public string FilePrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Output folder for DXF files and Excel workbook.
|
||||
/// Equipment number from the UI (e.g., "5028").
|
||||
/// </summary>
|
||||
public string OutputFolder { get; set; }
|
||||
public string Equipment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Drawing number from the UI (e.g., "A02", "Misc").
|
||||
/// </summary>
|
||||
public string DrawingNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional title/label for the export.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Selected Equipment ID for API operations (optional).
|
||||
/// </summary>
|
||||
public int? EquipmentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token for canceling the export operation.
|
||||
@@ -108,17 +123,23 @@ namespace ExportDXF.Services
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
// Close the document without saving
|
||||
SolidWorksApp.CloseDoc(title);
|
||||
ProgressCallback?.Invoke("Closed template drawing", LogLevel.Info, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the reference regardless of success/failure
|
||||
TemplateDrawing = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ProgressCallback?.Invoke($"Failed to close template drawing: {ex.Message}", LogLevel.Error, null);
|
||||
|
||||
// Still clear the reference to prevent further issues
|
||||
TemplateDrawing = null;
|
||||
|
||||
// Don't throw here as this is cleanup code - log the error but continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
ExportDXF/Models/ExportRecord.cs
Normal file
20
ExportDXF/Models/ExportRecord.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ExportDXF.Models
|
||||
{
|
||||
public class ExportRecord
|
||||
{
|
||||
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<BomItem> BomItems { get; set; } = new List<BomItem>();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
using ExportDXF.ApiClient;
|
||||
using ExportDXF.Forms;
|
||||
using ExportDXF.Services;
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace ExportDXF
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
@@ -19,32 +28,89 @@ namespace ExportDXF
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple dependency injection container.
|
||||
/// </summary>
|
||||
public class ServiceContainer
|
||||
{
|
||||
private readonly string _apiBaseUrl;
|
||||
|
||||
public ServiceContainer()
|
||||
{
|
||||
_apiBaseUrl = ConfigurationManager.AppSettings["FabWorksApiUrl"] ?? "http://localhost:5206";
|
||||
}
|
||||
|
||||
public MainForm ResolveMainForm()
|
||||
{
|
||||
var solidWorksService = new SolidWorksService();
|
||||
var bomExtractor = new BomExtractor();
|
||||
var partExporter = new PartExporter();
|
||||
var drawingExporter = new DrawingExporter();
|
||||
var excelExportService = new ExcelExportService();
|
||||
var logFileService = new LogFileService();
|
||||
|
||||
EnsureApiRunning();
|
||||
|
||||
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,
|
||||
excelExportService,
|
||||
logFileService);
|
||||
apiClient);
|
||||
|
||||
var extractors = new IDrawingInfoExtractor[]
|
||||
return new MainForm(solidWorksService, exportService, apiClient);
|
||||
}
|
||||
|
||||
private void EnsureApiRunning()
|
||||
{
|
||||
new EquipmentDrawingInfoExtractor(),
|
||||
new DefaultDrawingInfoExtractor()
|
||||
// Check if API is already responding
|
||||
using (var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(2) })
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = probe.GetAsync(_apiBaseUrl + "/api/exports?take=1").Result;
|
||||
if (response.IsSuccessStatusCode)
|
||||
return; // already running
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Find the API executable relative to this assembly
|
||||
var exeDir = AppContext.BaseDirectory;
|
||||
var apiExe = Path.GetFullPath(Path.Combine(exeDir, @"..\..\..\FabWorks.Api\bin\Debug\net8.0\FabWorks.Api.exe"));
|
||||
if (!File.Exists(apiExe))
|
||||
return; // can't find it, skip
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = apiExe,
|
||||
WorkingDirectory = Path.GetDirectoryName(apiExe),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
return new MainForm(solidWorksService, exportService, extractors);
|
||||
Process.Start(startInfo);
|
||||
|
||||
// Wait up to 10 seconds for API to become ready
|
||||
using (var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(2) })
|
||||
{
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
try
|
||||
{
|
||||
var response = probe.GetAsync(_apiBaseUrl + "/api/exports?take=1").Result;
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Fallback extractor that uses the document name as prefix
|
||||
/// and appends the configured default suffix.
|
||||
/// Always returns true — this is the catch-all.
|
||||
/// </summary>
|
||||
public class DefaultDrawingInfoExtractor : IDrawingInfoExtractor
|
||||
{
|
||||
public bool TryExtract(string documentName, out DrawingInfoResult info)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(documentName);
|
||||
var suffix = ConfigurationManager.AppSettings["DefaultSuffix"] ?? "PT{item_no:2}";
|
||||
|
||||
info = new DrawingInfoResult
|
||||
{
|
||||
EquipmentNumber = null,
|
||||
DrawingNumber = null,
|
||||
DefaultTemplate = $"{name} {suffix}"
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using ExportDXF.ApiClient;
|
||||
using ExportDXF.Extensions;
|
||||
using ExportDXF.ItemExtractors;
|
||||
using ExportDXF.Models;
|
||||
using ExportDXF.Utilities;
|
||||
using ExportDXF;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -13,34 +15,42 @@ namespace ExportDXF.Services
|
||||
{
|
||||
public interface IDxfExportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Exports the document specified in the context to DXF format.
|
||||
/// </summary>
|
||||
/// <param name="context">The export context containing all necessary information.</param>
|
||||
Task ExportAsync(ExportContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class DxfExportService : IDxfExportService
|
||||
{
|
||||
private readonly ISolidWorksService _solidWorksService;
|
||||
private readonly IBomExtractor _bomExtractor;
|
||||
private readonly IPartExporter _partExporter;
|
||||
private readonly IDrawingExporter _drawingExporter;
|
||||
private readonly ExcelExportService _excelExportService;
|
||||
private readonly LogFileService _logFileService;
|
||||
private readonly IFabWorksApiClient _apiClient;
|
||||
|
||||
public DxfExportService(
|
||||
ISolidWorksService solidWorksService,
|
||||
IBomExtractor bomExtractor,
|
||||
IPartExporter partExporter,
|
||||
IDrawingExporter drawingExporter,
|
||||
ExcelExportService excelExportService,
|
||||
LogFileService logFileService)
|
||||
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));
|
||||
_excelExportService = excelExportService ?? throw new ArgumentNullException(nameof(excelExportService));
|
||||
_logFileService = logFileService ?? throw new ArgumentNullException(nameof(logFileService));
|
||||
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports the document specified in the context to DXF format.
|
||||
/// </summary>
|
||||
public async Task ExportAsync(ExportContext context)
|
||||
{
|
||||
if (context == null)
|
||||
@@ -49,28 +59,6 @@ namespace ExportDXF.Services
|
||||
ValidateContext(context);
|
||||
SetupExportContext(context);
|
||||
|
||||
var outputFolder = context.OutputFolder;
|
||||
if (!Directory.Exists(outputFolder))
|
||||
Directory.CreateDirectory(outputFolder);
|
||||
|
||||
var prefix = FilenameTemplateParser.GetPrefix(
|
||||
context.FilenameTemplate,
|
||||
context.ActiveDocument.Title);
|
||||
|
||||
var xlsxPath = Path.Combine(outputFolder, $"{prefix}.xlsx");
|
||||
var logPath = Path.Combine(outputFolder, $"{prefix}.log");
|
||||
|
||||
_logFileService.StartExportLog(logPath);
|
||||
_logFileService.LogInfo($"Export started: {context.ActiveDocument.FilePath}");
|
||||
_logFileService.LogInfo($"Template: {context.FilenameTemplate}");
|
||||
_logFileService.LogInfo($"Output: {outputFolder}");
|
||||
|
||||
// Read existing cut templates for revision comparison
|
||||
var existingTemplates = _excelExportService.ReadExistingCutTemplates(xlsxPath);
|
||||
|
||||
var bomItems = new List<BomItem>();
|
||||
List<Dictionary<string, string>> rawBomTable = null;
|
||||
|
||||
var startTime = DateTime.Now;
|
||||
var tempDir = CreateTempWorkDir();
|
||||
|
||||
@@ -78,39 +66,26 @@ namespace ExportDXF.Services
|
||||
{
|
||||
_solidWorksService.EnableUserControl(false);
|
||||
|
||||
var drawingNumber = ParseDrawingNumber(context);
|
||||
|
||||
switch (context.ActiveDocument.DocumentType)
|
||||
{
|
||||
case DocumentType.Part:
|
||||
await ExportPartAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
|
||||
await ExportPartAsync(context, tempDir, drawingNumber);
|
||||
break;
|
||||
|
||||
case DocumentType.Assembly:
|
||||
await ExportAssemblyAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
|
||||
await ExportAssemblyAsync(context, tempDir, drawingNumber);
|
||||
break;
|
||||
|
||||
case DocumentType.Drawing:
|
||||
rawBomTable = await ExportDrawingAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
|
||||
await ExportDrawingAsync(context, drawingNumber, tempDir);
|
||||
break;
|
||||
|
||||
default:
|
||||
LogProgress(context, "Unknown document type.", LogLevel.Error);
|
||||
break;
|
||||
}
|
||||
|
||||
// Write Excel file
|
||||
_excelExportService.Write(xlsxPath, rawBomTable, bomItems);
|
||||
_logFileService.LogInfo($"Wrote {Path.GetFileName(xlsxPath)}");
|
||||
LogProgress(context, $"Saved {Path.GetFileName(xlsxPath)}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logFileService.LogWarning("Export cancelled by user");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logFileService.LogError($"Export failed: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -119,19 +94,13 @@ namespace ExportDXF.Services
|
||||
CleanupTempDir(tempDir);
|
||||
|
||||
var duration = DateTime.Now - startTime;
|
||||
_logFileService.LogInfo($"Run time: {duration.ToReadableFormat()}");
|
||||
LogProgress(context, $"Run time: {duration.ToReadableFormat()}");
|
||||
}
|
||||
}
|
||||
|
||||
#region Export Methods by Document Type
|
||||
|
||||
private async Task ExportPartAsync(
|
||||
ExportContext context,
|
||||
string tempDir,
|
||||
string outputFolder,
|
||||
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
|
||||
List<BomItem> bomItems)
|
||||
private async Task ExportPartAsync(ExportContext context, string tempDir, string drawingNumber)
|
||||
{
|
||||
LogProgress(context, "Active document is a Part");
|
||||
|
||||
@@ -142,25 +111,54 @@ namespace ExportDXF.Services
|
||||
return;
|
||||
}
|
||||
|
||||
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
|
||||
var item = _partExporter.ExportSinglePart(part, tempDir, context);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.ItemNo = "1";
|
||||
// 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var bomItem = CreateBomItem(item);
|
||||
PlaceDxfFile(item, context, outputFolder, existingTemplates, bomItem);
|
||||
bomItems.Add(bomItem);
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
|
||||
if (exportRecord != null)
|
||||
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportAssemblyAsync(
|
||||
ExportContext context,
|
||||
string tempDir,
|
||||
string outputFolder,
|
||||
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
|
||||
List<BomItem> bomItems)
|
||||
private async Task ExportAssemblyAsync(ExportContext context, string tempDir, string drawingNumber)
|
||||
{
|
||||
LogProgress(context, "Active document is an Assembly");
|
||||
LogProgress(context, "Fetching components...");
|
||||
@@ -182,63 +180,150 @@ namespace ExportDXF.Services
|
||||
|
||||
LogProgress(context, $"Found {items.Count} item(s).");
|
||||
|
||||
// Assign item numbers
|
||||
int nextNum = 1;
|
||||
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, outputFolder, context, existingTemplates, bomItems);
|
||||
}
|
||||
|
||||
private async Task<List<Dictionary<string, string>>> ExportDrawingAsync(
|
||||
ExportContext context,
|
||||
string tempDir,
|
||||
string outputFolder,
|
||||
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
|
||||
List<BomItem> bomItems)
|
||||
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
|
||||
}
|
||||
|
||||
private async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir)
|
||||
{
|
||||
LogProgress(context, "Active document is a Drawing");
|
||||
LogProgress(context, "Finding BOM tables...");
|
||||
|
||||
var drawing = context.ActiveDocument.NativeDocument as DrawingDoc;
|
||||
if (drawing == null)
|
||||
{
|
||||
LogProgress(context, "Failed to get drawing document.", LogLevel.Error);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Read raw BOM table for Excel output
|
||||
var rawBomTable = new List<Dictionary<string, string>>();
|
||||
var bomTables = drawing.GetBomTables();
|
||||
foreach (var table in bomTables)
|
||||
// Export drawing to PDF in temp dir
|
||||
_drawingExporter.ExportToPdf(drawing, tempDir, context);
|
||||
|
||||
// Create export record via API
|
||||
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
|
||||
|
||||
// Upload PDF to API with versioning
|
||||
try
|
||||
{
|
||||
var rows = RawBomTableReader.Read(table);
|
||||
rawBomTable.AddRange(rows);
|
||||
var pdfs = Directory.GetFiles(tempDir, "*.pdf");
|
||||
if (pdfs.Length > 0)
|
||||
{
|
||||
var pdfTempPath = pdfs[0];
|
||||
var pdfHash = ContentHasher.ComputePdfContentHash(pdfTempPath);
|
||||
|
||||
var uploadResult = await _apiClient.UploadPdfAsync(
|
||||
pdfTempPath,
|
||||
context.Equipment,
|
||||
context.DrawingNo,
|
||||
pdfHash,
|
||||
exportRecord?.Id);
|
||||
|
||||
if (exportRecord != null)
|
||||
await _apiClient.UpdatePdfHashAsync(exportRecord.Id, pdfHash);
|
||||
|
||||
if (uploadResult != null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// Extract items for DXF export
|
||||
// Extract BOM items from drawing tables
|
||||
LogProgress(context, "Finding BOM tables...");
|
||||
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
|
||||
|
||||
if (items == null || items.Count == 0)
|
||||
if (items != null && items.Count > 0)
|
||||
{
|
||||
LogProgress(context, "Error: Bill of materials not found.", LogLevel.Error);
|
||||
return rawBomTable;
|
||||
LogProgress(context, $"Found {items.Count} component(s)");
|
||||
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No BOM table — fall back to exporting the part referenced by the drawing views
|
||||
LogProgress(context, "No BOM table found. Checking drawing views for referenced part...");
|
||||
|
||||
var (part, configuration) = GetReferencedPartFromViews(drawing);
|
||||
if (part == null)
|
||||
{
|
||||
LogProgress(context, "No referenced part found in drawing views.", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
LogProgress(context, $"Found {items.Count} component(s)");
|
||||
LogProgress(context, $"Found referenced part, exporting as single part...");
|
||||
|
||||
// Export drawing to PDF in output folder
|
||||
_drawingExporter.ExportToPdf(drawing, outputFolder, context);
|
||||
var item = _partExporter.ExportSinglePart(part, tempDir, context);
|
||||
if (item != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configuration))
|
||||
item.Configuration = configuration;
|
||||
|
||||
await ExportItemsAsync(items, tempDir, outputFolder, context, existingTemplates, bomItems);
|
||||
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
|
||||
item.ItemNo = existingItemNo ?? await GetNextItemNumberAsync(drawingNumber);
|
||||
|
||||
return rawBomTable;
|
||||
var bomItem = new BomItem
|
||||
{
|
||||
ExportRecordId = exportRecord?.Id ?? 0,
|
||||
ItemNo = item.ItemNo,
|
||||
PartNo = item.FileName ?? item.PartName ?? "",
|
||||
SortOrder = 0,
|
||||
Qty = item.Quantity,
|
||||
TotalQty = item.Quantity,
|
||||
Description = item.Description ?? "",
|
||||
PartName = item.PartName ?? "",
|
||||
ConfigurationName = item.Configuration ?? "",
|
||||
Material = item.Material ?? ""
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(item.LocalTempPath))
|
||||
{
|
||||
var uploadResult = await UploadDxfAsync(item, context);
|
||||
if (uploadResult != null)
|
||||
{
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
{
|
||||
DxfFilePath = uploadResult.StoredFilePath,
|
||||
ContentHash = item.ContentHash,
|
||||
Thickness = item.Thickness > 0 ? item.Thickness : null,
|
||||
KFactor = item.KFactor > 0 ? item.KFactor : null,
|
||||
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
|
||||
if (exportRecord != null)
|
||||
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -247,12 +332,17 @@ namespace ExportDXF.Services
|
||||
|
||||
private void SetupExportContext(ExportContext context)
|
||||
{
|
||||
// Set up SolidWorks application reference
|
||||
context.SolidWorksApp = _solidWorksService.GetNativeSldWorks();
|
||||
|
||||
if (context.SolidWorksApp == null)
|
||||
{
|
||||
throw new InvalidOperationException("SolidWorks service is not connected.");
|
||||
}
|
||||
|
||||
// Set up drawing template path
|
||||
context.TemplateDrawing = null;
|
||||
|
||||
LogProgress(context, "Export context initialized");
|
||||
}
|
||||
|
||||
@@ -260,11 +350,13 @@ namespace ExportDXF.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
// Clean up template drawing if it was created
|
||||
context.CleanupTemplateDrawing();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogProgress(context, $"Warning: Failed to cleanup template drawing: {ex.Message}", LogLevel.Warning);
|
||||
// Don't throw - this is cleanup code
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +372,7 @@ namespace ExportDXF.Services
|
||||
{
|
||||
TopLevelOnly = false
|
||||
};
|
||||
|
||||
return extractor.GetItems();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -289,17 +382,10 @@ namespace ExportDXF.Services
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportItemsAsync(
|
||||
List<Item> items,
|
||||
string tempDir,
|
||||
string outputFolder,
|
||||
ExportContext context,
|
||||
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
|
||||
List<BomItem> bomItems)
|
||||
private async Task ExportItemsAsync(List<Item> items, string tempDir, ExportContext context, int? exportRecordId = null)
|
||||
{
|
||||
int successCount = 0;
|
||||
int skippedCount = 0;
|
||||
int unchangedCount = 0;
|
||||
int failureCount = 0;
|
||||
int sortOrder = 0;
|
||||
|
||||
@@ -313,130 +399,16 @@ namespace ExportDXF.Services
|
||||
|
||||
try
|
||||
{
|
||||
// PartExporter will handle template drawing creation through context
|
||||
_partExporter.ExportItem(item, tempDir, context);
|
||||
|
||||
var bomItem = CreateBomItem(item);
|
||||
bomItem.SortOrder = sortOrder++;
|
||||
|
||||
if (!string.IsNullOrEmpty(item.LocalTempPath))
|
||||
{
|
||||
var wasPlaced = PlaceDxfFile(item, context, outputFolder, existingTemplates, bomItem);
|
||||
if (wasPlaced)
|
||||
successCount++;
|
||||
else
|
||||
unchangedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
skippedCount++;
|
||||
}
|
||||
|
||||
bomItems.Add(bomItem);
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogProgress(context, $"Error exporting item {item.ItemNo}: {ex.Message}", LogLevel.Error);
|
||||
_logFileService.LogError($"Item {item.ItemNo}: {ex.Message}");
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
var summary = $"Export complete: {successCount} exported";
|
||||
if (unchangedCount > 0)
|
||||
summary += $", {unchangedCount} unchanged";
|
||||
if (skippedCount > 0)
|
||||
summary += $", {skippedCount} skipped (non-sheet-metal)";
|
||||
if (failureCount > 0)
|
||||
summary += $", {failureCount} failed";
|
||||
|
||||
LogProgress(context, summary, failureCount > 0 ? LogLevel.Warning : LogLevel.Info);
|
||||
_logFileService.LogInfo(summary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Places a DXF file in the output folder with revision tracking.
|
||||
/// Returns true if a new file was written, false if unchanged.
|
||||
/// </summary>
|
||||
private bool PlaceDxfFile(
|
||||
Item item,
|
||||
ExportContext context,
|
||||
string outputFolder,
|
||||
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
|
||||
BomItem bomItem)
|
||||
{
|
||||
var baseName = FilenameTemplateParser.Evaluate(context.FilenameTemplate, item);
|
||||
var contentHash = item.ContentHash;
|
||||
|
||||
int revision = 1;
|
||||
string dxfFileName;
|
||||
|
||||
// Check existing templates for revision comparison
|
||||
if (existingTemplates.TryGetValue(item.ItemNo, out var existing))
|
||||
{
|
||||
if (existing.ContentHash == contentHash)
|
||||
{
|
||||
// Unchanged — skip file write, keep existing
|
||||
dxfFileName = existing.FileName;
|
||||
revision = existing.Revision;
|
||||
|
||||
LogProgress(context, $"Unchanged: {dxfFileName}", LogLevel.Info, item.PartName);
|
||||
_logFileService.LogInfo($"Unchanged: {dxfFileName}");
|
||||
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
{
|
||||
DxfFilePath = dxfFileName,
|
||||
ContentHash = contentHash,
|
||||
Revision = revision,
|
||||
Thickness = item.Thickness > 0 ? item.Thickness : (double?)null,
|
||||
KFactor = item.KFactor > 0 ? item.KFactor : (double?)null,
|
||||
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : (double?)null
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Changed — increment revision
|
||||
revision = existing.Revision + 1;
|
||||
dxfFileName = GetRevisionFileName(baseName, revision);
|
||||
|
||||
LogProgress(context, $"Updated: {dxfFileName} (Rev{revision})", LogLevel.Info, item.PartName);
|
||||
_logFileService.LogInfo($"Updated: {dxfFileName} (was {existing.FileName})");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// New item
|
||||
dxfFileName = $"{baseName}.dxf";
|
||||
LogProgress(context, $"Exported: {dxfFileName}", LogLevel.Info, item.PartName);
|
||||
_logFileService.LogInfo($"Exported: {dxfFileName}");
|
||||
}
|
||||
|
||||
// Copy from temp to output
|
||||
var destPath = Path.Combine(outputFolder, dxfFileName);
|
||||
File.Copy(item.LocalTempPath, destPath, overwrite: true);
|
||||
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
{
|
||||
DxfFilePath = Path.GetFileNameWithoutExtension(dxfFileName),
|
||||
ContentHash = contentHash,
|
||||
Revision = revision,
|
||||
Thickness = item.Thickness > 0 ? item.Thickness : (double?)null,
|
||||
KFactor = item.KFactor > 0 ? item.KFactor : (double?)null,
|
||||
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : (double?)null
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private BomItem CreateBomItem(Item item)
|
||||
{
|
||||
return new BomItem
|
||||
// Always create BomItem for every item (sheet metal or not)
|
||||
var bomItem = new BomItem
|
||||
{
|
||||
ExportRecordId = exportRecordId ?? 0,
|
||||
ItemNo = item.ItemNo ?? "",
|
||||
PartNo = item.FileName ?? item.PartName ?? "",
|
||||
SortOrder = 0,
|
||||
SortOrder = sortOrder++,
|
||||
Qty = item.Quantity,
|
||||
TotalQty = item.Quantity,
|
||||
Description = item.Description ?? "",
|
||||
@@ -444,19 +416,198 @@ namespace ExportDXF.Services
|
||||
ConfigurationName = item.Configuration ?? "",
|
||||
Material = item.Material ?? ""
|
||||
};
|
||||
|
||||
// Only upload and create CutTemplate if DXF was exported successfully
|
||||
if (!string.IsNullOrEmpty(item.LocalTempPath))
|
||||
{
|
||||
successCount++;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
skippedCount++;
|
||||
}
|
||||
|
||||
// Add to UI
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
|
||||
// Save BOM item via API if we have an export record
|
||||
if (exportRecordId.HasValue)
|
||||
{
|
||||
await SaveBomItemAsync(exportRecordId.Value, bomItem, context);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogProgress(context, $"Error exporting item {item.ItemNo}: {ex.Message}", LogLevel.Error);
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
var summary = $"Export complete: {successCount} exported, {skippedCount} skipped";
|
||||
if (failureCount > 0)
|
||||
summary += $", {failureCount} failed";
|
||||
LogProgress(context, summary, failureCount > 0 ? LogLevel.Warning : LogLevel.Info);
|
||||
|
||||
if (exportRecordId.HasValue)
|
||||
{
|
||||
LogProgress(context, $"BOM items saved (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Upload
|
||||
|
||||
private async Task<ApiFileUploadResponse> UploadDxfAsync(Item item, ExportContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _apiClient.UploadDxfAsync(
|
||||
item.LocalTempPath,
|
||||
context.Equipment,
|
||||
context.DrawingNo,
|
||||
item.ItemNo,
|
||||
item.ContentHash);
|
||||
|
||||
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)
|
||||
{
|
||||
LogProgress(context, $"DXF upload failed for {item.FileName}: {ex.Message}", LogLevel.Warning);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region API Helpers
|
||||
|
||||
private async Task<ExportRecord> CreateExportRecordAsync(ExportContext context, string drawingNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
var record = new ExportRecord
|
||||
{
|
||||
Id = dto.Id,
|
||||
DrawingNumber = dto.DrawingNumber,
|
||||
EquipmentNo = dto.EquipmentNo,
|
||||
DrawingNo = dto.DrawingNo,
|
||||
SourceFilePath = dto.SourceFilePath,
|
||||
OutputFolder = dto.OutputFolder,
|
||||
ExportedAt = dto.ExportedAt,
|
||||
ExportedBy = dto.ExportedBy
|
||||
};
|
||||
|
||||
LogProgress(context, $"Created export record (ID: {record.Id})", LogLevel.Info);
|
||||
return record;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogProgress(context, $"API error creating export record: {ex.Message}", LogLevel.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> 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<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private string GetRevisionFileName(string baseName, int revision)
|
||||
{
|
||||
if (revision <= 1)
|
||||
return $"{baseName}.dxf";
|
||||
return $"{baseName} Rev{revision}.dxf";
|
||||
}
|
||||
|
||||
private string CreateTempWorkDir()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), "ExportDXF-" + Guid.NewGuid().ToString("N"));
|
||||
@@ -477,6 +628,27 @@ namespace ExportDXF.Services
|
||||
}
|
||||
}
|
||||
|
||||
private string ParseDrawingNumber(ExportContext context)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
var title = context?.ActiveDocument?.Title;
|
||||
info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title);
|
||||
}
|
||||
return info?.ToString();
|
||||
}
|
||||
|
||||
private void ValidateContext(ExportContext context)
|
||||
{
|
||||
if (context.ActiveDocument == null)
|
||||
@@ -484,12 +656,24 @@ namespace ExportDXF.Services
|
||||
|
||||
if (context.ProgressCallback == null)
|
||||
throw new ArgumentException("ProgressCallback cannot be null.", nameof(context));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(context.FilenameTemplate))
|
||||
throw new ArgumentException("FilenameTemplate cannot be null or empty.", nameof(context));
|
||||
private (PartDoc part, string configuration) GetReferencedPartFromViews(DrawingDoc drawing)
|
||||
{
|
||||
var view = (IView)drawing.GetFirstView();
|
||||
// First view is the sheet itself — skip it
|
||||
view = (IView)view.GetNextView();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(context.OutputFolder))
|
||||
throw new ArgumentException("OutputFolder cannot be null or empty.", nameof(context));
|
||||
while (view != null)
|
||||
{
|
||||
var doc = view.ReferencedDocument;
|
||||
if (doc is PartDoc part)
|
||||
return (part, view.ReferencedConfiguration);
|
||||
|
||||
view = (IView)view.GetNextView();
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts equipment/drawing numbers from document names matching the
|
||||
/// workplace format (e.g., "4321 A01.SLDDRW" → equipment "4321", drawing "A01").
|
||||
/// Uses the existing DrawingInfo.Parse() logic.
|
||||
/// </summary>
|
||||
public class EquipmentDrawingInfoExtractor : IDrawingInfoExtractor
|
||||
{
|
||||
public bool TryExtract(string documentName, out DrawingInfoResult info)
|
||||
{
|
||||
info = null;
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(documentName);
|
||||
var parsed = DrawingInfo.Parse(name);
|
||||
|
||||
if (parsed == null || string.IsNullOrEmpty(parsed.EquipmentNo))
|
||||
return false;
|
||||
|
||||
var template = !string.IsNullOrEmpty(parsed.DrawingNo)
|
||||
? $"{parsed.EquipmentNo} {parsed.DrawingNo} PT{{item_no:2}}"
|
||||
: $"{parsed.EquipmentNo} PT{{item_no:2}}";
|
||||
|
||||
info = new DrawingInfoResult
|
||||
{
|
||||
EquipmentNumber = parsed.EquipmentNo,
|
||||
DrawingNumber = parsed.DrawingNo,
|
||||
DefaultTemplate = template
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ClosedXML.Excel;
|
||||
using ExportDXF.Models;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
public class ExcelExportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads existing Cut Templates from an xlsx file to compare content hashes.
|
||||
/// Returns empty dictionary if file doesn't exist or has no Cut Templates sheet.
|
||||
/// Key = Item #, Value = (ContentHash, Revision, FileName)
|
||||
/// </summary>
|
||||
public Dictionary<string, (string ContentHash, int Revision, string FileName)> ReadExistingCutTemplates(string xlsxPath)
|
||||
{
|
||||
var result = new Dictionary<string, (string, int, string)>();
|
||||
|
||||
if (!File.Exists(xlsxPath))
|
||||
return result;
|
||||
|
||||
using (var workbook = new XLWorkbook(xlsxPath))
|
||||
{
|
||||
if (!workbook.TryGetWorksheet("Cut Templates", out var ws))
|
||||
return result;
|
||||
|
||||
var lastCol = ws.LastColumnUsed()?.ColumnNumber() ?? 0;
|
||||
var lastRow = ws.LastRowUsed()?.RowNumber() ?? 1;
|
||||
|
||||
if (lastCol == 0 || lastRow <= 1)
|
||||
return result;
|
||||
|
||||
var headers = new Dictionary<string, int>();
|
||||
for (int col = 1; col <= lastCol; col++)
|
||||
{
|
||||
var header = ws.Cell(1, col).GetString();
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
headers[header] = col;
|
||||
}
|
||||
|
||||
if (!headers.ContainsKey("Item #") || !headers.ContainsKey("Content Hash"))
|
||||
return result;
|
||||
|
||||
for (int row = 2; row <= lastRow; row++)
|
||||
{
|
||||
var itemNo = ws.Cell(row, headers["Item #"]).GetString();
|
||||
var hash = ws.Cell(row, headers["Content Hash"]).GetString();
|
||||
var revision = headers.ContainsKey("Revision")
|
||||
? ws.Cell(row, headers["Revision"]).GetValue<int>()
|
||||
: 1;
|
||||
var fileName = headers.ContainsKey("File Name")
|
||||
? ws.Cell(row, headers["File Name"]).GetString()
|
||||
: "";
|
||||
|
||||
if (!string.IsNullOrEmpty(itemNo))
|
||||
result[itemNo] = (hash, revision, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes or updates the xlsx file with BOM and Cut Templates sheets.
|
||||
/// rawBomTable: list of rows where each row is a dictionary of column name → value.
|
||||
/// If null or empty, the BOM sheet is not written (Part/Assembly exports).
|
||||
/// </summary>
|
||||
public void Write(
|
||||
string xlsxPath,
|
||||
List<Dictionary<string, string>> rawBomTable,
|
||||
List<BomItem> bomItems)
|
||||
{
|
||||
using (var workbook = File.Exists(xlsxPath)
|
||||
? new XLWorkbook(xlsxPath)
|
||||
: new XLWorkbook())
|
||||
{
|
||||
WriteBomSheet(workbook, rawBomTable);
|
||||
WriteCutTemplatesSheet(workbook, bomItems);
|
||||
workbook.SaveAs(xlsxPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteBomSheet(XLWorkbook workbook, List<Dictionary<string, string>> rawBomTable)
|
||||
{
|
||||
if (rawBomTable == null || rawBomTable.Count == 0)
|
||||
return;
|
||||
|
||||
if (workbook.TryGetWorksheet("BOM", out _))
|
||||
workbook.Worksheets.Delete("BOM");
|
||||
|
||||
var sheet = workbook.Worksheets.Add("BOM");
|
||||
var columns = rawBomTable[0].Keys.ToList();
|
||||
|
||||
for (int col = 0; col < columns.Count; col++)
|
||||
sheet.Cell(1, col + 1).Value = columns[col];
|
||||
|
||||
for (int row = 0; row < rawBomTable.Count; row++)
|
||||
{
|
||||
for (int col = 0; col < columns.Count; col++)
|
||||
{
|
||||
string value;
|
||||
rawBomTable[row].TryGetValue(columns[col], out value);
|
||||
sheet.Cell(row + 2, col + 1).Value = value ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
sheet.Columns().AdjustToContents();
|
||||
}
|
||||
|
||||
private void WriteCutTemplatesSheet(XLWorkbook workbook, List<BomItem> bomItems)
|
||||
{
|
||||
if (workbook.TryGetWorksheet("Cut Templates", out _))
|
||||
workbook.Worksheets.Delete("Cut Templates");
|
||||
|
||||
var sheet = workbook.Worksheets.Add("Cut Templates");
|
||||
|
||||
var headers = new[] { "Item #", "File Name", "Revision", "Thickness", "K-Factor", "Bend Radius", "Content Hash" };
|
||||
for (int col = 0; col < headers.Length; col++)
|
||||
sheet.Cell(1, col + 1).Value = headers[col];
|
||||
|
||||
int row = 2;
|
||||
foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo))
|
||||
{
|
||||
var ct = item.CutTemplate;
|
||||
sheet.Cell(row, 1).Value = item.ItemNo;
|
||||
sheet.Cell(row, 2).Value = ct.DxfFilePath;
|
||||
sheet.Cell(row, 3).Value = ct.Revision;
|
||||
sheet.Cell(row, 4).Value = ct.Thickness ?? 0;
|
||||
sheet.Cell(row, 5).Value = ct.KFactor ?? 0;
|
||||
sheet.Cell(row, 6).Value = ct.DefaultBendRadius ?? 0;
|
||||
sheet.Cell(row, 7).Value = ct.ContentHash ?? "";
|
||||
row++;
|
||||
}
|
||||
|
||||
sheet.Columns().AdjustToContents();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
public static class FilenameTemplateParser
|
||||
{
|
||||
private static readonly Regex PlaceholderPattern = new Regex(
|
||||
@"\{(?<name>\w+)(?::(?<pad>\d+))?\}",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates a template string for a given item.
|
||||
/// e.g. "4321 A01 PT{item_no:2}" with item_no=3 → "4321 A01 PT03"
|
||||
/// </summary>
|
||||
public static string Evaluate(string template, Item item)
|
||||
{
|
||||
return PlaceholderPattern.Replace(template, match =>
|
||||
{
|
||||
var name = match.Groups["name"].Value.ToLowerInvariant();
|
||||
var padStr = match.Groups["pad"].Value;
|
||||
int pad = string.IsNullOrEmpty(padStr) ? 0 : int.Parse(padStr);
|
||||
|
||||
string value;
|
||||
switch (name)
|
||||
{
|
||||
case "item_no":
|
||||
value = item.ItemNo ?? "0";
|
||||
if (pad > 0)
|
||||
value = value.PadLeft(pad, '0');
|
||||
break;
|
||||
case "part_name":
|
||||
value = item.PartName ?? "";
|
||||
break;
|
||||
case "config":
|
||||
value = item.Configuration ?? "";
|
||||
break;
|
||||
case "material":
|
||||
value = item.Material ?? "";
|
||||
break;
|
||||
default:
|
||||
value = match.Value;
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the literal prefix before the first placeholder.
|
||||
/// Used for naming the xlsx and log files.
|
||||
/// Falls back to documentName if prefix is empty.
|
||||
/// </summary>
|
||||
public static string GetPrefix(string template, string documentName)
|
||||
{
|
||||
var match = PlaceholderPattern.Match(template);
|
||||
if (!match.Success)
|
||||
return template.Trim();
|
||||
|
||||
var prefix = template.Substring(0, match.Index).Trim();
|
||||
if (string.IsNullOrEmpty(prefix))
|
||||
return Path.GetFileNameWithoutExtension(documentName);
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the template contains {item_no} to prevent filename collisions.
|
||||
/// </summary>
|
||||
public static bool Validate(string template, out string error)
|
||||
{
|
||||
error = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
{
|
||||
error = "Template cannot be empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasItemNo = PlaceholderPattern.Matches(template)
|
||||
.Cast<Match>()
|
||||
.Any(m => m.Groups["name"].Value.Equals("item_no", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!hasItemNo)
|
||||
{
|
||||
error = "Template must contain {item_no} or {item_no:N} to avoid filename collisions.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
public interface IDrawingInfoExtractor
|
||||
{
|
||||
bool TryExtract(string documentName, out DrawingInfoResult info);
|
||||
}
|
||||
|
||||
public class DrawingInfoResult
|
||||
{
|
||||
public string EquipmentNumber { get; set; }
|
||||
public string DrawingNumber { get; set; }
|
||||
public string DefaultTemplate { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
public class LogFileService : IDisposable
|
||||
{
|
||||
private StreamWriter _exportLog;
|
||||
private static readonly string AppLogPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"ExportDXF", "ExportDXF.log");
|
||||
|
||||
public void StartExportLog(string logFilePath)
|
||||
{
|
||||
_exportLog?.Dispose();
|
||||
|
||||
var dir = Path.GetDirectoryName(logFilePath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
_exportLog = new StreamWriter(logFilePath, append: true);
|
||||
}
|
||||
|
||||
public void Log(string level, string message)
|
||||
{
|
||||
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {message}";
|
||||
|
||||
_exportLog?.WriteLine(line);
|
||||
_exportLog?.Flush();
|
||||
|
||||
WriteAppLog(line);
|
||||
}
|
||||
|
||||
public void LogInfo(string message) => Log("INFO", message);
|
||||
public void LogWarning(string message) => Log("WARNING", message);
|
||||
public void LogError(string message) => Log("ERROR", message);
|
||||
|
||||
private void WriteAppLog(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(AppLogPath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
File.AppendAllText(AppLogPath, line + Environment.NewLine);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort app log — don't fail exports if log write fails
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_exportLog?.Dispose();
|
||||
_exportLog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ namespace ExportDXF.Services
|
||||
|
||||
try
|
||||
{
|
||||
var fileName = GetSinglePartFileName(model);
|
||||
var fileName = GetSinglePartFileName(model, context.Equipment);
|
||||
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
|
||||
|
||||
// Build result item with metadata
|
||||
@@ -139,7 +139,7 @@ namespace ExportDXF.Services
|
||||
|
||||
EnrichItemWithMetadata(item, model, part);
|
||||
|
||||
var fileName = GetItemFileName(item);
|
||||
var fileName = GetItemFileName(item, context);
|
||||
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
|
||||
|
||||
var templateDrawing = context.GetOrCreateTemplateDrawing();
|
||||
@@ -234,7 +234,7 @@ namespace ExportDXF.Services
|
||||
var drawingModel = templateDrawing as ModelDoc2;
|
||||
drawingModel.SaveAs(savePath);
|
||||
|
||||
AddEtchLines(savePath);
|
||||
AddEtchLines(savePath, context);
|
||||
|
||||
context.ProgressCallback?.Invoke($"Saved to \"{savePath}\"", LogLevel.Info, partTitle);
|
||||
|
||||
@@ -301,7 +301,7 @@ namespace ExportDXF.Services
|
||||
drawing.DeleteSelection(false);
|
||||
}
|
||||
|
||||
private void AddEtchLines(string dxfPath)
|
||||
private void AddEtchLines(string dxfPath, ExportContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -309,9 +309,9 @@ namespace ExportDXF.Services
|
||||
etcher.AddEtchLines(dxfPath);
|
||||
FixDegreeSymbol(dxfPath);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silently fail if etch lines can't be added
|
||||
context.ProgressCallback?.Invoke($"Etch lines failed: {ex.Message}", LogLevel.Warning, Path.GetFileName(dxfPath));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,22 +332,36 @@ namespace ExportDXF.Services
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSinglePartFileName(ModelDoc2 model)
|
||||
private string GetSinglePartFileName(ModelDoc2 model, string equipment)
|
||||
{
|
||||
var title = model.GetTitle().Replace(".SLDPRT", "");
|
||||
var config = model.ConfigurationManager.ActiveConfiguration.Name;
|
||||
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return isDefaultConfig ? title : $"{title} [{config}]";
|
||||
var name = isDefaultConfig ? title : $"{title} [{config}]";
|
||||
return string.IsNullOrWhiteSpace(equipment) ? name : $"{equipment} {name}";
|
||||
}
|
||||
|
||||
private string GetItemFileName(Item item)
|
||||
private string GetItemFileName(Item item, ExportContext context)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.ItemNo))
|
||||
return item.PartName ?? "unknown";
|
||||
if (string.IsNullOrWhiteSpace(context.DrawingNo))
|
||||
{
|
||||
// No drawing number: preserve part name, prefix with EquipmentNo
|
||||
var equipment = context.Equipment;
|
||||
return string.IsNullOrWhiteSpace(equipment)
|
||||
? item.PartName
|
||||
: $"{equipment} {item.PartName}";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.ItemNo))
|
||||
return item.PartName;
|
||||
|
||||
var prefix = context.FilePrefix?.Replace("\"", "''") ?? string.Empty;
|
||||
var num = item.ItemNo.PadLeft(2, '0');
|
||||
return $"PT{num}";
|
||||
// Expected format: {EquipNo} {DrawingNo} PT{ItemNo}
|
||||
return string.IsNullOrWhiteSpace(prefix)
|
||||
? $"PT{num}"
|
||||
: $"{prefix} PT{num}";
|
||||
}
|
||||
|
||||
private void LogExportFailure(Item item, ExportContext context)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads all visible columns and rows from a SolidWorks BOM table annotation
|
||||
/// as raw string data for direct copy into an Excel sheet.
|
||||
/// </summary>
|
||||
public static class RawBomTableReader
|
||||
{
|
||||
public static List<Dictionary<string, string>> Read(BomTableAnnotation bomTable)
|
||||
{
|
||||
var table = (TableAnnotation)bomTable;
|
||||
var rows = new List<Dictionary<string, string>>();
|
||||
|
||||
int colCount = table.ColumnCount;
|
||||
int rowCount = table.RowCount;
|
||||
|
||||
// Build visible column headers
|
||||
var columns = new List<(int Index, string Header)>();
|
||||
for (int col = 0; col < colCount; col++)
|
||||
{
|
||||
if (table.ColumnHidden[col])
|
||||
continue;
|
||||
|
||||
var header = table.get_Text(0, col)?.Trim() ?? $"Column{col}";
|
||||
columns.Add((col, header));
|
||||
}
|
||||
|
||||
// Read data rows (skip header row 0, skip hidden rows)
|
||||
for (int row = 1; row < rowCount; row++)
|
||||
{
|
||||
if (table.RowHidden[row])
|
||||
continue;
|
||||
|
||||
var rowData = new Dictionary<string, string>();
|
||||
foreach (var (colIdx, header) in columns)
|
||||
{
|
||||
rowData[header] = table.get_Text(row, colIdx)?.Trim() ?? "";
|
||||
}
|
||||
rows.Add(rowData);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using PDFtoImage;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace ExportDXF.Utilities
|
||||
{
|
||||
public static class ContentHasher
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a SHA256 hash of DXF file content, skipping the HEADER section
|
||||
/// which contains timestamps that change on every save.
|
||||
/// Computes a SHA256 hash of DXF geometry, ignoring entity ordering,
|
||||
/// handle assignments, style names, and floating-point epsilon differences
|
||||
/// that SolidWorks changes between re-exports of identical geometry.
|
||||
/// Falls back to a raw file hash if ACadSharp parsing fails.
|
||||
/// </summary>
|
||||
public static string ComputeDxfContentHash(string filePath)
|
||||
{
|
||||
var text = File.ReadAllText(filePath);
|
||||
var contentStart = FindEndOfHeader(text);
|
||||
var content = contentStart >= 0 ? text.Substring(contentStart) : text;
|
||||
|
||||
using (var sha = SHA256.Create())
|
||||
try
|
||||
{
|
||||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content));
|
||||
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
|
||||
return ComputeGeometricHash(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ComputeFileHash(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a perceptual hash of a PDF by rendering page 1 to an image,
|
||||
/// so only visual changes affect the hash (metadata/timestamp changes are ignored).
|
||||
/// Falls back to a raw file hash if rendering fails.
|
||||
/// </summary>
|
||||
public static string ComputePdfContentHash(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var pdfStream = File.OpenRead(filePath))
|
||||
using (var pngStream = new MemoryStream())
|
||||
{
|
||||
Conversion.SavePng(pngStream, pdfStream, page: 0,
|
||||
options: new RenderOptions(Dpi: 150));
|
||||
pngStream.Position = 0;
|
||||
|
||||
using (var image = Image.Load<Rgba32>(pngStream))
|
||||
{
|
||||
var hash = ComputeDifferenceHash(image);
|
||||
return hash.ToString("x16");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ComputeFileHash(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,81 +77,126 @@ namespace ExportDXF.Utilities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the position immediately after the HEADER section's ENDSEC marker.
|
||||
/// DXF HEADER format:
|
||||
/// 0\nSECTION\n2\nHEADER\n...variables...\n0\nENDSEC\n
|
||||
/// Returns -1 if no HEADER section is found.
|
||||
/// DifferenceHash: resize to 9x8 grayscale, compare adjacent pixels.
|
||||
/// Produces a 64-bit hash. Implemented directly against ImageSharp 3.x API
|
||||
/// (CoenM.ImageHash uses the removed GetPixelRowSpan from ImageSharp 2.x).
|
||||
/// </summary>
|
||||
private static int FindEndOfHeader(string text)
|
||||
private static ulong ComputeDifferenceHash(Image<Rgba32> image)
|
||||
{
|
||||
// Find the HEADER section start
|
||||
var headerIndex = FindGroupCode(text, 0, "2", "HEADER");
|
||||
if (headerIndex < 0)
|
||||
return -1;
|
||||
// Resize to 9 wide x 8 tall for 8x8 = 64 bit comparisons
|
||||
image.Mutate(ctx => ctx.Resize(9, 8));
|
||||
|
||||
// Advance past the HEADER value line so pair scanning stays aligned
|
||||
var headerLineEnd = text.IndexOf('\n', headerIndex);
|
||||
if (headerLineEnd < 0)
|
||||
return -1;
|
||||
ulong hash = 0;
|
||||
int bit = 0;
|
||||
|
||||
// Find the ENDSEC that closes the HEADER section
|
||||
var pos = headerLineEnd + 1;
|
||||
while (pos < text.Length)
|
||||
for (int y = 0; y < 8; y++)
|
||||
{
|
||||
var endsecIndex = FindGroupCode(text, pos, "0", "ENDSEC");
|
||||
if (endsecIndex < 0)
|
||||
return -1;
|
||||
for (int x = 0; x < 8; x++)
|
||||
{
|
||||
var left = image[x, y];
|
||||
var right = image[x + 1, y];
|
||||
var leftGray = 0.299 * left.R + 0.587 * left.G + 0.114 * left.B;
|
||||
var rightGray = 0.299 * right.R + 0.587 * right.G + 0.114 * right.B;
|
||||
|
||||
// Move past the ENDSEC line
|
||||
var lineEnd = text.IndexOf('\n', endsecIndex);
|
||||
return lineEnd >= 0 ? lineEnd + 1 : text.Length;
|
||||
if (leftGray > rightGray)
|
||||
hash |= (1UL << bit);
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a DXF group code pair (code line followed by value line) starting from the given position.
|
||||
/// Returns the position of the value line, or -1 if not found.
|
||||
/// </summary>
|
||||
private static int FindGroupCode(string text, int startIndex, string groupCode, string value)
|
||||
private static string ComputeGeometricHash(string filePath)
|
||||
{
|
||||
var pos = startIndex;
|
||||
while (pos < text.Length)
|
||||
using (var reader = new DxfReader(filePath))
|
||||
{
|
||||
// Skip whitespace/newlines to find the group code
|
||||
while (pos < text.Length && (text[pos] == '\r' || text[pos] == '\n' || text[pos] == ' '))
|
||||
pos++;
|
||||
var doc = reader.Read();
|
||||
var signatures = new List<string>();
|
||||
|
||||
if (pos >= text.Length)
|
||||
break;
|
||||
|
||||
// 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;
|
||||
foreach (var entity in doc.Entities)
|
||||
{
|
||||
signatures.Add(GetEntitySignature(entity));
|
||||
}
|
||||
|
||||
return -1;
|
||||
signatures.Sort(StringComparer.Ordinal);
|
||||
var combined = string.Join("\n", signatures);
|
||||
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(combined));
|
||||
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetEntitySignature(Entity entity)
|
||||
{
|
||||
var layer = entity.Layer?.Name ?? "";
|
||||
|
||||
switch (entity)
|
||||
{
|
||||
case Line line:
|
||||
return GetLineSignature(line, layer);
|
||||
case Arc arc:
|
||||
return GetArcSignature(arc, layer);
|
||||
case Circle circle:
|
||||
return GetCircleSignature(circle, layer);
|
||||
case MText mtext:
|
||||
return GetMTextSignature(mtext, layer);
|
||||
default:
|
||||
return $"{entity.GetType().Name}|{layer}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetLineSignature(Line line, string layer)
|
||||
{
|
||||
var p1 = FormatPoint(line.StartPoint.X, line.StartPoint.Y);
|
||||
var p2 = FormatPoint(line.EndPoint.X, line.EndPoint.Y);
|
||||
|
||||
// Normalize endpoint order so direction doesn't affect the hash
|
||||
if (string.Compare(p1, p2, StringComparison.Ordinal) > 0)
|
||||
{
|
||||
var tmp = p1;
|
||||
p1 = p2;
|
||||
p2 = tmp;
|
||||
}
|
||||
|
||||
return $"LINE|{layer}|{p1}|{p2}";
|
||||
}
|
||||
|
||||
private static string GetArcSignature(Arc arc, string layer)
|
||||
{
|
||||
var center = FormatPoint(arc.Center.X, arc.Center.Y);
|
||||
var r = R(arc.Radius);
|
||||
var sa = R(arc.StartAngle);
|
||||
var ea = R(arc.EndAngle);
|
||||
return $"ARC|{layer}|{center}|{r}|{sa}|{ea}";
|
||||
}
|
||||
|
||||
private static string GetCircleSignature(Circle circle, string layer)
|
||||
{
|
||||
var center = FormatPoint(circle.Center.X, circle.Center.Y);
|
||||
var r = R(circle.Radius);
|
||||
return $"CIRCLE|{layer}|{center}|{r}";
|
||||
}
|
||||
|
||||
private static string GetMTextSignature(MText mtext, string layer)
|
||||
{
|
||||
var point = FormatPoint(mtext.InsertPoint.X, mtext.InsertPoint.Y);
|
||||
var text = mtext.Value ?? "";
|
||||
return $"MTEXT|{layer}|{point}|{text}";
|
||||
}
|
||||
|
||||
private static string R(double value)
|
||||
{
|
||||
return Math.Round(value, 4).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string FormatPoint(double x, double y)
|
||||
{
|
||||
return $"{R(x)},{R(y)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
|
||||
</startup>
|
||||
<appSettings>
|
||||
<add key="MaxBendRadius" value="2.0"/>
|
||||
<add key="DefaultSuffix" value="PT{item_no:2}"/>
|
||||
<add key="FabWorksApiUrl" value="http://localhost:5206"/>
|
||||
</appSettings>
|
||||
</configuration>
|
||||
|
||||
9
FabWorks.Api/Configuration/FileStorageOptions.cs
Normal file
9
FabWorks.Api/Configuration/FileStorageOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace FabWorks.Api.Configuration
|
||||
{
|
||||
public class FileStorageOptions
|
||||
{
|
||||
public const string SectionName = "FileStorage";
|
||||
|
||||
public string OutputFolder { get; set; } = @"C:\ExportDXF\Output";
|
||||
}
|
||||
}
|
||||
259
FabWorks.Api/Controllers/BomItemsController.cs
Normal file
259
FabWorks.Api/Controllers/BomItemsController.cs
Normal file
@@ -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<ActionResult<BomItemDto>> 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<ActionResult<List<BomItemDto>>> 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<ActionResult<BomItemDto>> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private async Task<int> 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
470
FabWorks.Api/Controllers/ExportsController.cs
Normal file
470
FabWorks.Api/Controllers/ExportsController.cs
Normal file
@@ -0,0 +1,470 @@
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Numerics;
|
||||
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<ActionResult<object>> 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<ActionResult<ExportDetailDto>> 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<ActionResult<ExportDetailDto>> 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<ActionResult<ExportDetailDto>> 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<ActionResult<List<ExportDetailDto>>> 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<ActionResult<string>> 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<ActionResult<List<string>>> 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<ActionResult<List<string>>> 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<ActionResult<List<string>>> 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<ActionResult<string>> 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<IActionResult> UpdatePdfHash(int id, [FromBody] UpdatePdfHashRequest request)
|
||||
{
|
||||
var record = await _db.ExportRecords.FindAsync(id);
|
||||
if (record == null) return NotFound();
|
||||
|
||||
record.PdfContentHash = request.PdfContentHash;
|
||||
|
||||
if (!string.IsNullOrEmpty(record.DrawingNumber) && !string.IsNullOrEmpty(request.PdfContentHash))
|
||||
{
|
||||
var (drawing, revision) = await ResolveDrawingAsync(record.DrawingNumber, record.Title, request.PdfContentHash);
|
||||
record.Drawing = drawing;
|
||||
record.DrawingRevision = revision;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task<(Drawing drawing, int revision)> ResolveDrawingAsync(string drawingNumber, string title, string pdfContentHash)
|
||||
{
|
||||
var drawing = await _db.Drawings
|
||||
.FirstOrDefaultAsync(d => d.DrawingNumber == drawingNumber);
|
||||
|
||||
// Get the highest revision recorded for this drawing across all exports
|
||||
var lastRevision = await _db.ExportRecords
|
||||
.Where(r => r.DrawingNumber == drawingNumber && r.DrawingRevision != null)
|
||||
.OrderByDescending(r => r.DrawingRevision)
|
||||
.Select(r => r.DrawingRevision)
|
||||
.FirstOrDefaultAsync() ?? 0;
|
||||
|
||||
if (drawing == null)
|
||||
{
|
||||
drawing = new Drawing
|
||||
{
|
||||
DrawingNumber = drawingNumber,
|
||||
Title = title,
|
||||
PdfContentHash = pdfContentHash
|
||||
};
|
||||
_db.Drawings.Add(drawing);
|
||||
return (drawing, 1);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
drawing.Title = title;
|
||||
|
||||
if (ArePerceptualHashesSimilar(drawing.PdfContentHash, pdfContentHash))
|
||||
{
|
||||
// Hash unchanged — keep same revision
|
||||
return (drawing, lastRevision == 0 ? 1 : lastRevision);
|
||||
}
|
||||
|
||||
// Hash changed — bump revision and update stored hash
|
||||
drawing.PdfContentHash = pdfContentHash;
|
||||
return (drawing, lastRevision + 1);
|
||||
}
|
||||
|
||||
[HttpGet("previous-cut-template")]
|
||||
public async Task<ActionResult<CutTemplateDto>> GetPreviousCutTemplate(
|
||||
[FromQuery] string drawingNumber,
|
||||
[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<IActionResult> Delete(int id)
|
||||
{
|
||||
var record = await _db.ExportRecords
|
||||
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
|
||||
.Include(r => r.BomItems).ThenInclude(b => b.FormProgram)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
if (record == null) return NotFound();
|
||||
|
||||
_db.ExportRecords.Remove(record);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/download-dxfs")]
|
||||
public async Task<IActionResult> DownloadAllDxfs(int id)
|
||||
{
|
||||
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 zipName = $"{record.DrawingNumber ?? $"Export-{id}"} DXFs.zip";
|
||||
return BuildDxfZip(dxfItems, zipName);
|
||||
}
|
||||
|
||||
[HttpGet("download-dxfs")]
|
||||
public async Task<IActionResult> DownloadDxfsByDrawing([FromQuery] string drawingNumber)
|
||||
{
|
||||
if (string.IsNullOrEmpty(drawingNumber))
|
||||
return BadRequest("drawingNumber is required.");
|
||||
|
||||
var dxfItems = await _db.BomItems
|
||||
.Include(b => b.CutTemplate)
|
||||
.Where(b => b.ExportRecord.DrawingNumber == drawingNumber
|
||||
&& b.CutTemplate != null
|
||||
&& b.CutTemplate.ContentHash != null)
|
||||
.ToListAsync();
|
||||
|
||||
if (dxfItems.Count == 0) return NotFound("No DXF files for this drawing.");
|
||||
|
||||
// Deduplicate by content hash (keep latest)
|
||||
dxfItems = dxfItems
|
||||
.GroupBy(b => b.CutTemplate.ContentHash)
|
||||
.Select(g => g.Last())
|
||||
.ToList();
|
||||
|
||||
var zipName = $"{drawingNumber} DXFs.zip";
|
||||
return BuildDxfZip(dxfItems, zipName);
|
||||
}
|
||||
|
||||
private FileResult BuildDxfZip(List<BomItem> dxfItems, string zipName)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
||||
{
|
||||
var usedNames = new HashSet<string>(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();
|
||||
blobStream.CopyTo(entryStream);
|
||||
blobStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
ms.Position = 0;
|
||||
return File(ms, "application/zip", zipName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two perceptual hashes using Hamming distance.
|
||||
/// Perceptual hashes (16 hex chars / 64 bits) are compared with a tolerance
|
||||
/// of up to 10 differing bits (~84% similarity). SHA256 fallback hashes
|
||||
/// (64 hex chars) use exact comparison.
|
||||
/// </summary>
|
||||
private static bool ArePerceptualHashesSimilar(string hash1, string hash2)
|
||||
{
|
||||
if (hash1 == hash2) return true;
|
||||
if (string.IsNullOrEmpty(hash1) || string.IsNullOrEmpty(hash2)) return false;
|
||||
|
||||
// Perceptual hashes are 16 hex chars (64-bit DifferenceHash)
|
||||
// SHA256 fallback hashes are 64 hex chars — require exact match
|
||||
if (hash1.Length != 16 || hash2.Length != 16)
|
||||
return false;
|
||||
|
||||
if (ulong.TryParse(hash1, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var h1) &&
|
||||
ulong.TryParse(hash2, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var h2))
|
||||
{
|
||||
var hammingDistance = BitOperations.PopCount(h1 ^ h2);
|
||||
return hammingDistance <= 10;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ExportDetailDto MapToDto(ExportRecord r) => new()
|
||||
{
|
||||
Id = r.Id,
|
||||
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()
|
||||
};
|
||||
}
|
||||
}
|
||||
189
FabWorks.Api/Controllers/FileBrowserController.cs
Normal file
189
FabWorks.Api/Controllers/FileBrowserController.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
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<ActionResult<FileListResult>> ListFiles(
|
||||
[FromQuery] string search = null,
|
||||
[FromQuery] string type = null)
|
||||
{
|
||||
var files = new List<StoredFileEntry>();
|
||||
|
||||
// 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,
|
||||
c.Revision,
|
||||
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<string>();
|
||||
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,
|
||||
Revision = c.Revision,
|
||||
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,
|
||||
r.DrawingRevision
|
||||
});
|
||||
|
||||
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<string>();
|
||||
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,
|
||||
Revision = r.DrawingRevision,
|
||||
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<StoredFileEntry> 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 int? Revision { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
}
|
||||
93
FabWorks.Api/Controllers/FilesController.cs
Normal file
93
FabWorks.Api/Controllers/FilesController.cs
Normal file
@@ -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<ActionResult<FileUploadResponse>> 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, file.FileName);
|
||||
|
||||
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<ActionResult<FileUploadResponse>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
FabWorks.Api/Controllers/FormProgramsController.cs
Normal file
106
FabWorks.Api/Controllers/FormProgramsController.cs
Normal file
@@ -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<ActionResult<List<FormProgramDto>>> 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<FormProgramDto> 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<ActionResult<FormProgramDto>> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
17
FabWorks.Api/DTOs/CreateExportRequest.cs
Normal file
17
FabWorks.Api/DTOs/CreateExportRequest.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
62
FabWorks.Api/DTOs/ExportDetailDto.cs
Normal file
62
FabWorks.Api/DTOs/ExportDetailDto.cs
Normal file
@@ -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<BomItemDto> 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; }
|
||||
}
|
||||
}
|
||||
11
FabWorks.Api/DTOs/FileUploadResponse.cs
Normal file
11
FabWorks.Api/DTOs/FileUploadResponse.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
20
FabWorks.Api/FabWorks.Api.csproj
Normal file
20
FabWorks.Api/FabWorks.Api.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FabWorks.Core\FabWorks.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
26
FabWorks.Api/Program.cs
Normal file
26
FabWorks.Api/Program.cs
Normal file
@@ -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<FabWorksDbContext>(options =>
|
||||
options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb")));
|
||||
builder.Services.AddSingleton<FormProgramService>();
|
||||
|
||||
builder.Services.Configure<FileStorageOptions>(
|
||||
builder.Configuration.GetSection(FileStorageOptions.SectionName));
|
||||
builder.Services.AddScoped<IFileStorageService, FileStorageService>();
|
||||
|
||||
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();
|
||||
41
FabWorks.Api/Properties/launchSettings.json
Normal file
41
FabWorks.Api/Properties/launchSettings.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
FabWorks.Api/Services/FileStorageService.cs
Normal file
167
FabWorks.Api/Services/FileStorageService.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
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<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash, string originalFileName = null);
|
||||
Task<FileUploadResult> StorePdfAsync(Stream stream, string equipment, string drawingNo, string contentHash, int? exportRecordId = null);
|
||||
Stream OpenBlob(string contentHash, string extension);
|
||||
bool BlobExists(string contentHash, string extension);
|
||||
}
|
||||
|
||||
public class FileStorageService : IFileStorageService
|
||||
{
|
||||
private readonly FileStorageOptions _options;
|
||||
private readonly FabWorksDbContext _db;
|
||||
|
||||
public string OutputFolder => _options.OutputFolder;
|
||||
|
||||
public FileStorageService(IOptions<FileStorageOptions> 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<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash, string originalFileName = null)
|
||||
{
|
||||
var fileName = BuildDxfFileName(drawingNo, equipment, itemNo, originalFileName);
|
||||
|
||||
// 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<FileUploadResult> 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<bool> 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, string originalFileName = null)
|
||||
{
|
||||
// No drawing number: use the original filename from the client
|
||||
if (string.IsNullOrEmpty(drawingNo) && !string.IsNullOrEmpty(originalFileName))
|
||||
{
|
||||
return originalFileName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)
|
||||
? originalFileName
|
||||
: originalFileName + ".dxf";
|
||||
}
|
||||
|
||||
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
|
||||
var paddedItem = (itemNo ?? "").PadLeft(2, '0');
|
||||
if (!string.IsNullOrEmpty(drawingNumber) && !string.IsNullOrEmpty(itemNo))
|
||||
return $"{drawingNumber} PT{paddedItem}.dxf";
|
||||
return $"PT{paddedItem}.dxf";
|
||||
}
|
||||
}
|
||||
}
|
||||
39
FabWorks.Api/Services/FormProgramService.cs
Normal file
39
FabWorks.Api/Services/FormProgramService.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
8
FabWorks.Api/appsettings.Development.json
Normal file
8
FabWorks.Api/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
FabWorks.Api/appsettings.json
Normal file
15
FabWorks.Api/appsettings.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
824
FabWorks.Api/wwwroot/css/styles.css
Normal file
824
FabWorks.Api/wwwroot/css/styles.css
Normal file
@@ -0,0 +1,824 @@
|
||||
: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; }
|
||||
|
||||
/* ─── Modal ─── */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
animation: fadeIn 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
.modal-panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
width: 90%;
|
||||
max-width: 640px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: fadeSlideIn 0.2s ease forwards;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.02em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.modal-header svg { width: 16px; height: 16px; }
|
||||
|
||||
.modal-body {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-green {
|
||||
background: var(--green-dim);
|
||||
color: var(--green);
|
||||
border-color: rgba(6, 118, 71, 0.25);
|
||||
}
|
||||
|
||||
.btn-green:hover {
|
||||
background: rgba(6, 118, 71, 0.15);
|
||||
border-color: rgba(6, 118, 71, 0.4);
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
/* ─── Toast ─── */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--text);
|
||||
color: #fff;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
z-index: 300;
|
||||
animation: fadeSlideIn 0.2s ease, fadeOut 0.3s ease 2s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut { to { opacity: 0; } }
|
||||
|
||||
/* ─── Responsive ─── */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
.main { margin-left: 0; }
|
||||
.search-box { width: 100%; }
|
||||
.topbar { padding: 0 16px; }
|
||||
.page-content { padding: 16px; }
|
||||
}
|
||||
56
FabWorks.Api/wwwroot/index.html
Normal file
56
FabWorks.Api/wwwroot/index.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FabWorks</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 20V8l4-4h6l2 2h6a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z"/>
|
||||
<path d="M8 10v6" opacity="0.5"/>
|
||||
<path d="M12 8v8" opacity="0.5"/>
|
||||
<path d="M16 11v3" opacity="0.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a class="nav-item active" data-page="exports" onclick="router.go('exports')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
<span class="nav-tooltip">Exports</span>
|
||||
</a>
|
||||
<a class="nav-item" data-page="drawings" onclick="router.go('drawings')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>
|
||||
<span class="nav-tooltip">Drawings</span>
|
||||
</a>
|
||||
<a class="nav-item" data-page="files" onclick="router.go('files')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
<span class="nav-tooltip">Files</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="topbar-left">
|
||||
<h2 id="page-title">Exports</h2>
|
||||
<span class="topbar-tag" id="page-tag"></span>
|
||||
</div>
|
||||
<div id="topbar-actions"></div>
|
||||
</div>
|
||||
<div class="page-content" id="page-content"></div>
|
||||
</div>
|
||||
|
||||
<script src="js/icons.js?v=3"></script>
|
||||
<script src="js/helpers.js?v=3"></script>
|
||||
<script src="js/components.js?v=3"></script>
|
||||
<script src="js/pages.js?v=3"></script>
|
||||
<script src="js/router.js?v=3"></script>
|
||||
<script>router.init();</script>
|
||||
</body>
|
||||
</html>
|
||||
155
FabWorks.Api/wwwroot/js/components.js
Normal file
155
FabWorks.Api/wwwroot/js/components.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/* ─── BOM Detail Expansion ─── */
|
||||
function renderBomDetails(b) {
|
||||
let html = '<div class="bom-expand-content">';
|
||||
|
||||
if (b.cutTemplate) {
|
||||
const ct = b.cutTemplate;
|
||||
const displayName = ct.dxfFilePath?.split(/[/\\]/).pop() || '';
|
||||
html += `
|
||||
<div class="bom-section-title">${icons.laser} Cut Template</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="lbl">File</span><span class="val">${esc(displayName)}</span></div>
|
||||
<div class="info-item"><span class="lbl">Thickness</span><span class="val">${fmtThickness(ct.thickness)}</span></div>
|
||||
<div class="info-item"><span class="lbl">K-Factor</span><span class="val">${ct.kFactor != null ? ct.kFactor : '\u2014'}</span></div>
|
||||
<div class="info-item"><span class="lbl">Bend Radius</span><span class="val">${ct.defaultBendRadius != null ? ct.defaultBendRadius.toFixed(4) + '"' : '\u2014'}</span></div>
|
||||
</div>`;
|
||||
|
||||
if (ct.contentHash) {
|
||||
html += `<div style="margin-top:10px">
|
||||
<a class="btn btn-cyan btn-sm" href="/api/files/blob/${encodeURIComponent(ct.contentHash)}?ext=dxf&download=true&name=${encodeURIComponent(displayName)}" onclick="event.stopPropagation()">${icons.download} Download DXF</a>
|
||||
<span style="font-family:var(--font-mono);font-size:13px;color:var(--text-dim);margin-left:8px">${esc(displayName)}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (b.formProgram) {
|
||||
const fp = b.formProgram;
|
||||
html += `
|
||||
<div class="bom-section-title">${icons.bend} Form Program</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="lbl">Program</span><span class="val">${esc(fp.programName)}</span></div>
|
||||
<div class="info-item"><span class="lbl">Thickness</span><span class="val">${fmtThickness(fp.thickness)}</span></div>
|
||||
<div class="info-item"><span class="lbl">Material</span><span class="val">${esc(fp.materialType)}</span></div>
|
||||
<div class="info-item"><span class="lbl">K-Factor</span><span class="val">${fp.kFactor != null ? fp.kFactor : '\u2014'}</span></div>
|
||||
<div class="info-item"><span class="lbl">Bends</span><span class="val">${fp.bendCount}</span></div>
|
||||
<div class="info-item"><span class="lbl">Upper Tools</span><span class="val">${esc(fp.upperToolNames) || '\u2014'}</span></div>
|
||||
<div class="info-item"><span class="lbl">Lower Tools</span><span class="val">${esc(fp.lowerToolNames) || '\u2014'}</span></div>
|
||||
</div>
|
||||
${fp.setupNotes ? `<div style="margin-top:8px;padding:8px 12px;background:var(--amber-dim);border-radius:4px;font-size:13px;font-family:var(--font-mono);color:var(--amber)"><span class="lbl">Setup Notes</span>${esc(fp.setupNotes)}</div>` : ''}`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
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);
|
||||
}
|
||||
|
||||
/* ─── Cut List Modal ─── */
|
||||
function showCutListModal(bomItems) {
|
||||
const cutItems = bomItems.filter(b => b.cutTemplate);
|
||||
if (cutItems.length === 0) {
|
||||
showToast('No cut templates found');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = cutItems.map(b => {
|
||||
const ct = b.cutTemplate;
|
||||
const name = ct.cutTemplateName || ct.dxfFilePath?.split(/[/\\]/).pop()?.replace(/\.dxf$/i, '') || b.partName || '';
|
||||
const qty = b.qty ?? '';
|
||||
return { name, qty };
|
||||
});
|
||||
|
||||
const tableRows = rows.map((r, i) => `
|
||||
<tr style="animation: fadeSlideIn 0.15s ease ${0.02 * i}s forwards; opacity: 0">
|
||||
<td style="font-family:var(--font-mono);font-weight:600">${esc(r.name)}</td>
|
||||
<td style="font-family:var(--font-mono);text-align:center">${r.qty}</td>
|
||||
</tr>`).join('');
|
||||
|
||||
// Remove existing modal if any
|
||||
const existing = document.getElementById('cut-list-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'cut-list-modal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-panel">
|
||||
<div class="modal-header">
|
||||
<span>${icons.laser} Cut List</span>
|
||||
<span class="badge badge-count">${cutItems.length} templates</span>
|
||||
<span style="margin-left:auto;display:flex;gap:6px">
|
||||
<button class="btn btn-cyan btn-sm" onclick="copyCutList()" id="copy-cut-list-btn">${icons.clipboard} Copy</button>
|
||||
<button class="btn btn-sm" onclick="closeCutListModal()">${icons.close}</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Name</th>
|
||||
<th style="width:60px;text-align:center">Qty</th>
|
||||
</tr></thead>
|
||||
<tbody>${tableRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
// Store data for copy
|
||||
modal._cutData = rows;
|
||||
// Close on backdrop click
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeCutListModal(); });
|
||||
// Close on Escape
|
||||
modal._keyHandler = e => { if (e.key === 'Escape') closeCutListModal(); };
|
||||
document.addEventListener('keydown', modal._keyHandler);
|
||||
}
|
||||
|
||||
function closeCutListModal() {
|
||||
const modal = document.getElementById('cut-list-modal');
|
||||
if (!modal) return;
|
||||
document.removeEventListener('keydown', modal._keyHandler);
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
function copyCutList() {
|
||||
const modal = document.getElementById('cut-list-modal');
|
||||
if (!modal || !modal._cutData) return;
|
||||
|
||||
const text = modal._cutData.map(r => `${r.name}\t${r.qty}`).join('\n');
|
||||
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const btn = document.getElementById('copy-cut-list-btn');
|
||||
if (btn) {
|
||||
btn.innerHTML = `${icons.check} Copied!`;
|
||||
btn.classList.remove('btn-cyan');
|
||||
btn.classList.add('btn-green');
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = `${icons.clipboard} Copy`;
|
||||
btn.classList.remove('btn-green');
|
||||
btn.classList.add('btn-cyan');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(msg) {
|
||||
const t = document.createElement('div');
|
||||
t.className = 'toast';
|
||||
t.textContent = msg;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(() => t.remove(), 2500);
|
||||
}
|
||||
50
FabWorks.Api/wwwroot/js/helpers.js
Normal file
50
FabWorks.Api/wwwroot/js/helpers.js
Normal file
@@ -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 `<span style="font-family:var(--font-mono)">${t.toFixed(4)}"</span>`;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return s ? s.replace(/</g,'<').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);
|
||||
}
|
||||
}
|
||||
23
FabWorks.Api/wwwroot/js/icons.js
Normal file
23
FabWorks.Api/wwwroot/js/icons.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const icons = {
|
||||
search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
|
||||
folder: `<svg viewBox="0 0 24 24" fill="var(--amber-dim)" stroke="var(--amber)" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
|
||||
fileDxf: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--cyan)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
|
||||
filePdf: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--red)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
|
||||
fileGeneric: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
|
||||
download: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
|
||||
back: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>`,
|
||||
chevron: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`,
|
||||
laser: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--cyan)" stroke-width="1.2"><circle cx="8" cy="8" r="2"/><path d="M8 2v3M8 11v3M2 8h3M11 8h3" opacity="0.5"/></svg>`,
|
||||
bend: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.2"><path d="M3 13V7a4 4 0 0 1 4-4h6"/><polyline points="10 6 13 3 10 0" transform="translate(0,2)"/></svg>`,
|
||||
trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
|
||||
clipboard: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="2" width="6" height="4" rx="1"/><path d="M9 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-2"/><line x1="9" y1="12" x2="15" y2="12"/><line x1="9" y1="16" x2="15" y2="16"/></svg>`,
|
||||
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>`,
|
||||
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
|
||||
};
|
||||
|
||||
function fileIcon(name) {
|
||||
const ext = name.split('.').pop().toLowerCase();
|
||||
if (ext === 'dxf') return icons.fileDxf;
|
||||
if (ext === 'pdf') return icons.filePdf;
|
||||
return icons.fileGeneric;
|
||||
}
|
||||
393
FabWorks.Api/wwwroot/js/pages.js
Normal file
393
FabWorks.Api/wwwroot/js/pages.js
Normal file
@@ -0,0 +1,393 @@
|
||||
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 = `
|
||||
<div class="search-box">
|
||||
${icons.search}
|
||||
<input type="text" id="export-search" placeholder="Search drawing, part, user..." value="${esc(searchVal)}">
|
||||
</div>`;
|
||||
|
||||
content.innerHTML = `<div class="loading">Loading exports</div>`;
|
||||
|
||||
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 = `<div class="empty">No exports found.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
setPage('Exports', `${data.items.length} exports`);
|
||||
|
||||
const rows = data.items.map((e, i) => `
|
||||
<tr class="clickable" onclick="router.go('drawing-detail', {id: '${encodeURIComponent(e.drawingNumber)}', eid: '${e.id}'})" style="animation: fadeSlideIn 0.2s ease ${0.02 * Math.min(i, 25)}s forwards; opacity: 0">
|
||||
<td style="font-family:var(--font-mono);color:var(--text-dim);font-size:13px">${e.id}</td>
|
||||
<td><strong>${esc(e.drawingNumber) || '<span style="color:var(--text-dim)">\u2014</span>'}</strong></td>
|
||||
<td style="color:var(--text-secondary);font-size:13px">${esc(e.title) || ''}</td>
|
||||
<td><span class="badge badge-count">${e.bomItemCount}</span></td>
|
||||
<td style="color:var(--text-secondary)">${esc(e.exportedBy)}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary);white-space:nowrap">${fmtDate(e.exportedAt)}</td>
|
||||
<td><button class="btn btn-red btn-sm" onclick="event.stopPropagation();deleteExport(${e.id})">${icons.trash}</button></td>
|
||||
</tr>`).join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="card animate-in">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:50px">#</th>
|
||||
<th>Drawing</th>
|
||||
<th>Title</th>
|
||||
<th style="width:80px">Items</th>
|
||||
<th>Exported By</th>
|
||||
<th style="width:180px">Date</th>
|
||||
<th style="width:50px"></th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
async drawings(params) {
|
||||
const actions = document.getElementById('topbar-actions');
|
||||
const content = document.getElementById('page-content');
|
||||
setPage('Drawings');
|
||||
|
||||
const searchVal = (params && params.q) || '';
|
||||
actions.innerHTML = `
|
||||
<div class="search-box">
|
||||
${icons.search}
|
||||
<input type="text" id="drawing-search" placeholder="Search drawing, part, user..." value="${esc(searchVal)}">
|
||||
</div>`;
|
||||
|
||||
content.innerHTML = `<div class="loading">Loading drawings</div>`;
|
||||
|
||||
const searchInput = document.getElementById('drawing-search');
|
||||
let debounce;
|
||||
searchInput.addEventListener('input', () => {
|
||||
clearTimeout(debounce);
|
||||
debounce = setTimeout(() => router.go('drawings', { q: searchInput.value }), 400);
|
||||
});
|
||||
|
||||
try {
|
||||
const searchQ = searchVal ? `&search=${encodeURIComponent(searchVal)}` : '';
|
||||
const data = await api.get(`/api/exports?take=500${searchQ}`);
|
||||
|
||||
if (data.items.length === 0) {
|
||||
content.innerHTML = `<div class="empty">No drawings found.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate: keep only the latest export per drawing number
|
||||
const seen = new Set();
|
||||
const unique = data.items.filter(e => {
|
||||
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 `
|
||||
<tr class="clickable" onclick="router.go('drawing-detail', {id: '${encodeURIComponent(e.drawingNumber)}'})" style="animation: fadeSlideIn 0.2s ease ${0.02 * i}s forwards; opacity: 0">
|
||||
<td><strong>${esc(drawingPart) || '<span style="color:var(--text-dim)">\u2014</span>'}</strong></td>
|
||||
<td style="color:var(--text-secondary);font-size:13px">${esc(e.title) || ''}</td>
|
||||
<td><span class="badge badge-count">${e.bomItemCount}</span></td>
|
||||
<td style="color:var(--text-secondary)">${esc(e.exportedBy)}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary);white-space:nowrap">${fmtDate(e.exportedAt)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="equip-group animate-in" id="equip-${esc(equip)}" style="animation-delay:${0.04 * gi}s">
|
||||
<div class="equip-header" onclick="toggleEquipGroup('equip-${esc(equip)}')">
|
||||
<span class="chevron-toggle open" id="equip-${esc(equip)}-icon">${icons.chevron}</span>
|
||||
<span class="equip-header-number">${esc(equip)}</span>
|
||||
<div class="equip-header-meta">
|
||||
<span class="equip-header-stat"><strong>${items.length}</strong> drawings</span>
|
||||
<span class="equip-header-stat"><strong>${totalBom}</strong> items</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="equip-body">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Drawing</th>
|
||||
<th>Title</th>
|
||||
<th style="width:80px">Items</th>
|
||||
<th>Exported By</th>
|
||||
<th style="width:180px">Latest Export</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card animate-in"><div class="stat-label">Drawings</div><div class="stat-value">${uniqueDrawings}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">Equipment</div><div class="stat-value">${uniqueEquip}</div></div>
|
||||
</div>
|
||||
${groupsHtml}`;
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
async drawingDetail(drawingEncoded, params) {
|
||||
const drawingNumber = decodeURIComponent(drawingEncoded);
|
||||
const exportId = params?.eid ? parseInt(params.eid) : null;
|
||||
const actions = document.getElementById('topbar-actions');
|
||||
const content = document.getElementById('page-content');
|
||||
setPage(drawingNumber, 'drawing');
|
||||
actions.innerHTML = '';
|
||||
content.innerHTML = `<div class="loading">Loading drawing</div>`;
|
||||
|
||||
try {
|
||||
const exports = await api.get(`/api/exports/by-drawing?drawingNumber=${encodeURIComponent(drawingNumber)}`);
|
||||
|
||||
if (exports.length === 0) {
|
||||
content.innerHTML = `
|
||||
<a class="back-link" onclick="router.go('drawings')">${icons.back} Back to drawings</a>
|
||||
<div class="empty">No exports found for this drawing.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let allBom;
|
||||
const singleExport = exportId ? exports.find(e => e.id === exportId) : null;
|
||||
if (singleExport) {
|
||||
// Viewing a specific export - show only its BOM items
|
||||
allBom = (singleExport.bomItems || []).map(b => ({ ...b, exportId: singleExport.id, exportedAt: singleExport.exportedAt }));
|
||||
} else {
|
||||
// Viewing drawing overview - deduplicate by itemNo, keeping latest revision (exports are newest-first)
|
||||
const bomByItem = new Map();
|
||||
exports.forEach(exp => {
|
||||
(exp.bomItems || []).forEach(b => {
|
||||
if (!bomByItem.has(b.itemNo)) {
|
||||
bomByItem.set(b.itemNo, { ...b, exportId: exp.id, exportedAt: exp.exportedAt });
|
||||
}
|
||||
});
|
||||
});
|
||||
allBom = [...bomByItem.values()];
|
||||
}
|
||||
|
||||
// Store for cut list modal
|
||||
window._currentBom = allBom;
|
||||
|
||||
const bomRows = allBom.map((b, i) => {
|
||||
const hasDetails = b.cutTemplate || b.formProgram;
|
||||
const toggleId = `dbom-${b.id}`;
|
||||
return `
|
||||
<tr class="${hasDetails ? 'clickable' : ''}" ${hasDetails ? `onclick="toggleBomRow('${toggleId}')"` : ''} style="animation: fadeSlideIn 0.25s ease ${0.03 * i}s forwards; opacity: 0">
|
||||
<td style="width:32px">${hasDetails ? `<span class="chevron-toggle" id="${toggleId}-icon">${icons.chevron}</span>` : ''}</td>
|
||||
<td style="font-family:var(--font-mono);font-weight:600;color:var(--cyan)">${esc(b.itemNo)}</td>
|
||||
<td><strong>${esc(b.partName)}</strong></td>
|
||||
<td style="font-family:var(--font-mono);text-align:center;font-size:13px">${b.cutTemplate?.revision ?? ''}</td>
|
||||
<td style="color:var(--text-secondary)">${esc(b.description)}</td>
|
||||
<td><span style="font-family:var(--font-mono);font-size:13px">${esc(b.material)}</span></td>
|
||||
<td style="font-family:var(--font-mono);text-align:center">${b.qty ?? ''}</td>
|
||||
<td style="font-family:var(--font-mono);text-align:center">${b.totalQty ?? ''}</td>
|
||||
<td>
|
||||
${b.cutTemplate ? `<span class="badge badge-cyan">${icons.laser} DXF</span>` : ''}
|
||||
${b.formProgram ? `<span class="badge badge-amber">${icons.bend} Form</span>` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
${hasDetails ? `<tr class="bom-expand-row" id="${toggleId}" style="display:none"><td colspan="9">${renderBomDetails(b)}</td></tr>` : ''}`;
|
||||
}).join('');
|
||||
|
||||
const backLink = singleExport
|
||||
? `<a class="back-link" onclick="router.go('exports')">${icons.back} Back to exports</a>`
|
||||
: `<a class="back-link" onclick="router.go('drawings')">${icons.back} Back to drawings</a>`;
|
||||
|
||||
const statsHtml = singleExport
|
||||
? `<div class="stats-grid">
|
||||
<div class="stat-card animate-in"><div class="stat-label">Exported By</div><div class="stat-value stat-sm">${esc(singleExport.exportedBy)}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">BOM Items</div><div class="stat-value">${allBom.length}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">Exported</div><div class="stat-value stat-sm">${fmtDate(singleExport.exportedAt)}</div></div>
|
||||
</div>`
|
||||
: `<div class="stats-grid">
|
||||
<div class="stat-card animate-in"><div class="stat-label">Exports</div><div class="stat-value">${exports.length}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">BOM Items</div><div class="stat-value">${allBom.length}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">Latest Export</div><div class="stat-value stat-sm">${fmtDate(exports[0].exportedAt)}</div></div>
|
||||
</div>`;
|
||||
|
||||
const bomHeader = singleExport ? 'BOM Items' : 'All BOM Items';
|
||||
const activeExport = singleExport || exports[0];
|
||||
const dxfCount = allBom.filter(b => b.cutTemplate?.contentHash).length;
|
||||
const pdfHash = activeExport.pdfContentHash;
|
||||
const pdfName = encodeURIComponent((drawingNumber || 'drawing') + '.pdf');
|
||||
|
||||
content.innerHTML = `
|
||||
${backLink}
|
||||
${statsHtml}
|
||||
|
||||
<div class="card animate-in">
|
||||
<div class="card-header">
|
||||
${bomHeader}
|
||||
<span class="badge badge-count">${allBom.length} items</span>
|
||||
<span style="margin-left:auto;display:flex;gap:6px">
|
||||
${dxfCount > 0 ? `<button class="btn btn-sm" onclick="showCutListModal(window._currentBom)">${icons.clipboard} Cut List</button>` : ''}
|
||||
${pdfHash ? `<a class="btn btn-amber btn-sm" href="/api/filebrowser/download?hash=${encodeURIComponent(pdfHash)}&ext=pdf&name=${pdfName}">${icons.download} PDF</a>` : ''}
|
||||
${dxfCount > 0 ? (singleExport
|
||||
? `<a class="btn btn-cyan btn-sm" href="/api/exports/${activeExport.id}/download-dxfs">${icons.download} All DXFs</a>`
|
||||
: `<a class="btn btn-cyan btn-sm" href="/api/exports/download-dxfs?drawingNumber=${encodeURIComponent(drawingNumber)}">${icons.download} All DXFs</a>`
|
||||
) : ''}
|
||||
</span>
|
||||
</div>
|
||||
${allBom.length ? `
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:32px"></th>
|
||||
<th style="width:60px">Item</th>
|
||||
<th>Part Name</th>
|
||||
<th style="width:45px;text-align:center">Rev</th>
|
||||
<th>Description</th>
|
||||
<th>Material</th>
|
||||
<th style="width:50px;text-align:center">Qty</th>
|
||||
<th style="width:55px;text-align:center">Total</th>
|
||||
<th style="width:120px">Data</th>
|
||||
</tr></thead>
|
||||
<tbody>${bomRows}</tbody>
|
||||
</table>` : '<div class="empty">No BOM items.</div>'}
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
async files(params) {
|
||||
const actions = document.getElementById('topbar-actions');
|
||||
const content = document.getElementById('page-content');
|
||||
setPage('Files');
|
||||
|
||||
const searchVal = params.q || '';
|
||||
actions.innerHTML = `
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<div class="search-box">
|
||||
${icons.search}
|
||||
<input type="text" id="file-search" placeholder="Search drawing number, filename..." value="${esc(searchVal)}">
|
||||
</div>
|
||||
<select id="file-type-filter" style="background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:6px 10px;color:var(--text);font-family:var(--font-body);font-size:14px;height:36px">
|
||||
<option value="">All types</option>
|
||||
<option value="dxf">DXF only</option>
|
||||
<option value="pdf">PDF only</option>
|
||||
</select>
|
||||
</div>`;
|
||||
|
||||
content.innerHTML = `<div class="loading">Loading files</div>`;
|
||||
|
||||
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 = `<div class="empty">No files found.</div>`;
|
||||
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 `
|
||||
<tr style="animation: fadeSlideIn 0.25s ease ${0.02 * i}s forwards; opacity: 0">
|
||||
<td><div class="file-name-cell">${ext === 'pdf' ? icons.filePdf : icons.fileDxf}<a href="/api/filebrowser/download?hash=${encodeURIComponent(f.contentHash)}&ext=${ext}&name=${encodeURIComponent(f.fileName)}">${esc(f.fileName)}</a></div></td>
|
||||
<td><span class="badge ${ext === 'dxf' ? 'badge-cyan' : 'badge-amber'}">${ext.toUpperCase()}</span></td>
|
||||
<td style="color:var(--text-secondary)">${esc(f.drawingNumber)}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;text-align:center;color:var(--text-secondary)">${f.revision != null ? f.revision : '\u2014'}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary)">${f.thickness != null ? f.thickness.toFixed(4) + '"' : '\u2014'}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary)">${fmtDate(f.createdAt)}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:12px;color:var(--text-dim)">${esc(hashShort)}</td>
|
||||
<td style="white-space:nowrap">
|
||||
<a class="btn btn-cyan btn-sm" href="/api/filebrowser/download?hash=${encodeURIComponent(f.contentHash)}&ext=${ext}&name=${encodeURIComponent(f.fileName)}">${icons.download}</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="card animate-in">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Name</th>
|
||||
<th style="width:60px">Type</th>
|
||||
<th>Drawing</th>
|
||||
<th style="width:45px;text-align:center">Rev</th>
|
||||
<th style="width:90px">Thickness</th>
|
||||
<th style="width:170px">Date</th>
|
||||
<th style="width:100px">Hash</th>
|
||||
<th style="width:90px">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
36
FabWorks.Api/wwwroot/js/router.js
Normal file
36
FabWorks.Api/wwwroot/js/router.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const router = {
|
||||
go(page, params = {}) {
|
||||
const qParts = [];
|
||||
if (params.q) qParts.push('q=' + encodeURIComponent(params.q));
|
||||
if (params.eid) qParts.push('eid=' + encodeURIComponent(params.eid));
|
||||
const hash = page + (params.id ? '/' + params.id : '') + (qParts.length ? '?' + qParts.join('&') : '');
|
||||
location.hash = hash;
|
||||
},
|
||||
parse() {
|
||||
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 === 'drawing-detail' && el.dataset.page === 'drawings'));
|
||||
});
|
||||
switch(page) {
|
||||
case 'exports': pages.exports(params); 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
94
FabWorks.Core/Data/FabWorksDbContext.cs
Normal file
94
FabWorks.Core/Data/FabWorksDbContext.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using FabWorks.Core.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FabWorks.Core.Data
|
||||
{
|
||||
public class FabWorksDbContext : DbContext
|
||||
{
|
||||
public DbSet<ExportRecord> ExportRecords { get; set; }
|
||||
public DbSet<BomItem> BomItems { get; set; }
|
||||
public DbSet<CutTemplate> CutTemplates { get; set; }
|
||||
public DbSet<FormProgram> FormPrograms { get; set; }
|
||||
public DbSet<Drawing> Drawings { get; set; }
|
||||
|
||||
public FabWorksDbContext(DbContextOptions<FabWorksDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<ExportRecord>(entity =>
|
||||
{
|
||||
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);
|
||||
entity.Property(e => e.PdfContentHash).HasMaxLength(64);
|
||||
|
||||
entity.HasMany(e => e.BomItems)
|
||||
.WithOne(b => b.ExportRecord)
|
||||
.HasForeignKey(b => b.ExportRecordId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(e => e.Drawing)
|
||||
.WithMany(d => d.ExportRecords)
|
||||
.HasForeignKey(e => e.DrawingId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<BomItem>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.ID);
|
||||
entity.Property(e => e.ItemNo).HasMaxLength(50);
|
||||
entity.Property(e => e.PartNo).HasMaxLength(100);
|
||||
entity.Property(e => e.Description).HasMaxLength(500);
|
||||
entity.Property(e => e.PartName).HasMaxLength(200);
|
||||
entity.Property(e => e.ConfigurationName).HasMaxLength(100);
|
||||
entity.Property(e => e.Material).HasMaxLength(100);
|
||||
|
||||
entity.HasOne(e => e.CutTemplate)
|
||||
.WithOne(ct => ct.BomItem)
|
||||
.HasForeignKey<CutTemplate>(ct => ct.BomItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(e => e.FormProgram)
|
||||
.WithOne(fp => fp.BomItem)
|
||||
.HasForeignKey<FormProgram>(fp => fp.BomItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<CutTemplate>(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<FormProgram>(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);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Drawing>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.DrawingNumber).HasMaxLength(100);
|
||||
entity.Property(e => e.Title).HasMaxLength(200);
|
||||
entity.Property(e => e.PdfContentHash).HasMaxLength(64);
|
||||
entity.HasIndex(e => e.DrawingNumber).IsUnique();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
17
FabWorks.Core/FabWorks.Core.csproj
Normal file
17
FabWorks.Core/FabWorks.Core.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
270
FabWorks.Core/Migrations/20260218171742_InitialCreate.Designer.cs
generated
Normal file
270
FabWorks.Core/Migrations/20260218171742_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,270 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260218171742_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.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
|
||||
}
|
||||
}
|
||||
}
|
||||
151
FabWorks.Core/Migrations/20260218171742_InitialCreate.cs
Normal file
151
FabWorks.Core/Migrations/20260218171742_InitialCreate.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExportRecords",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DrawingNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
EquipmentNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
DrawingNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
SourceFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
OutputFolder = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
ExportedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ExportedBy = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
PdfContentHash = table.Column<string>(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<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ItemNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
PartNo = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false),
|
||||
Qty = table.Column<int>(type: "int", nullable: true),
|
||||
TotalQty = table.Column<int>(type: "int", nullable: true),
|
||||
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
PartName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
ConfigurationName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
Material = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
ExportRecordId = table.Column<int>(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<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DxfFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
CutTemplateName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
Thickness = table.Column<double>(type: "float", nullable: true),
|
||||
KFactor = table.Column<double>(type: "float", nullable: true),
|
||||
DefaultBendRadius = table.Column<double>(type: "float", nullable: true),
|
||||
BomItemId = table.Column<int>(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<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ProgramFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
ProgramName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
Thickness = table.Column<double>(type: "float", nullable: true),
|
||||
MaterialType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
KFactor = table.Column<double>(type: "float", nullable: true),
|
||||
BendCount = table.Column<int>(type: "int", nullable: false),
|
||||
UpperToolNames = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
LowerToolNames = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
SetupNotes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
BomItemId = table.Column<int>(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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CutTemplates");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "FormPrograms");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BomItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExportRecords");
|
||||
}
|
||||
}
|
||||
}
|
||||
273
FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs
generated
Normal file
273
FabWorks.Core/Migrations/20260219134027_AddCutTemplateRevision.Designer.cs
generated
Normal file
@@ -0,0 +1,273 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260219134027_AddCutTemplateRevision")]
|
||||
partial class AddCutTemplateRevision
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCutTemplateRevision : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Revision",
|
||||
table: "CutTemplates",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Revision",
|
||||
table: "CutTemplates");
|
||||
}
|
||||
}
|
||||
}
|
||||
325
FabWorks.Core/Migrations/20260220125334_AddDrawingEntity.Designer.cs
generated
Normal file
325
FabWorks.Core/Migrations/20260220125334_AddDrawingEntity.Designer.cs
generated
Normal file
@@ -0,0 +1,325 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260220125334_AddDrawingEntity")]
|
||||
partial class AddDrawingEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("[DrawingNumber] IS NOT NULL");
|
||||
|
||||
b.ToTable("Drawings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("DrawingId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingId");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
|
||||
.WithMany("ExportRecords")
|
||||
.HasForeignKey("DrawingId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Drawing");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Navigation("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
75
FabWorks.Core/Migrations/20260220125334_AddDrawingEntity.cs
Normal file
75
FabWorks.Core/Migrations/20260220125334_AddDrawingEntity.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDrawingEntity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DrawingId",
|
||||
table: "ExportRecords",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Drawings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DrawingNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
PdfContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
Revision = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Drawings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExportRecords_DrawingId",
|
||||
table: "ExportRecords",
|
||||
column: "DrawingId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Drawings_DrawingNumber",
|
||||
table: "Drawings",
|
||||
column: "DrawingNumber",
|
||||
unique: true,
|
||||
filter: "[DrawingNumber] IS NOT NULL");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ExportRecords_Drawings_DrawingId",
|
||||
table: "ExportRecords",
|
||||
column: "DrawingId",
|
||||
principalTable: "Drawings",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ExportRecords_Drawings_DrawingId",
|
||||
table: "ExportRecords");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Drawings");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ExportRecords_DrawingId",
|
||||
table: "ExportRecords");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DrawingId",
|
||||
table: "ExportRecords");
|
||||
}
|
||||
}
|
||||
}
|
||||
325
FabWorks.Core/Migrations/20260220130029_SeedDrawingsFromExistingExports.Designer.cs
generated
Normal file
325
FabWorks.Core/Migrations/20260220130029_SeedDrawingsFromExistingExports.Designer.cs
generated
Normal file
@@ -0,0 +1,325 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260220130029_SeedDrawingsFromExistingExports")]
|
||||
partial class SeedDrawingsFromExistingExports
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("[DrawingNumber] IS NOT NULL");
|
||||
|
||||
b.ToTable("Drawings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("DrawingId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingId");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
|
||||
.WithMany("ExportRecords")
|
||||
.HasForeignKey("DrawingId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Drawing");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Navigation("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeedDrawingsFromExistingExports : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Create Drawing records from existing ExportRecords (latest hash per DrawingNumber)
|
||||
migrationBuilder.Sql(@"
|
||||
INSERT INTO Drawings (DrawingNumber, Title, PdfContentHash, Revision)
|
||||
SELECT
|
||||
sub.DrawingNumber,
|
||||
sub.Title,
|
||||
sub.PdfContentHash,
|
||||
1
|
||||
FROM (
|
||||
SELECT
|
||||
e.DrawingNumber,
|
||||
e.Title,
|
||||
e.PdfContentHash,
|
||||
ROW_NUMBER() OVER (PARTITION BY e.DrawingNumber ORDER BY e.Id DESC) AS rn
|
||||
FROM ExportRecords e
|
||||
WHERE e.DrawingNumber IS NOT NULL
|
||||
AND e.PdfContentHash IS NOT NULL
|
||||
) sub
|
||||
WHERE sub.rn = 1;
|
||||
");
|
||||
|
||||
// Link ExportRecords to their Drawing
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE er
|
||||
SET er.DrawingId = d.Id
|
||||
FROM ExportRecords er
|
||||
INNER JOIN Drawings d ON d.DrawingNumber = er.DrawingNumber
|
||||
WHERE er.PdfContentHash IS NOT NULL;
|
||||
");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("UPDATE ExportRecords SET DrawingId = NULL;");
|
||||
migrationBuilder.Sql("DELETE FROM Drawings;");
|
||||
}
|
||||
}
|
||||
}
|
||||
325
FabWorks.Core/Migrations/20260220171747_MoveRevisionFromDrawingToExportRecord.Designer.cs
generated
Normal file
325
FabWorks.Core/Migrations/20260220171747_MoveRevisionFromDrawingToExportRecord.Designer.cs
generated
Normal file
@@ -0,0 +1,325 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260220171747_MoveRevisionFromDrawingToExportRecord")]
|
||||
partial class MoveRevisionFromDrawingToExportRecord
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("[DrawingNumber] IS NOT NULL");
|
||||
|
||||
b.ToTable("Drawings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("DrawingId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("DrawingRevision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingId");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
|
||||
.WithMany("ExportRecords")
|
||||
.HasForeignKey("DrawingId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Drawing");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Navigation("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MoveRevisionFromDrawingToExportRecord : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Revision",
|
||||
table: "Drawings");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DrawingRevision",
|
||||
table: "ExportRecords",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DrawingRevision",
|
||||
table: "ExportRecords");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Revision",
|
||||
table: "Drawings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
322
FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs
Normal file
322
FabWorks.Core/Migrations/FabWorksDbContextModelSnapshot.cs
Normal file
@@ -0,0 +1,322 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
partial class FabWorksDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("[DrawingNumber] IS NOT NULL");
|
||||
|
||||
b.ToTable("Drawings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("DrawingId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("DrawingRevision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingId");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
|
||||
.WithMany("ExportRecords")
|
||||
.HasForeignKey("DrawingId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Drawing");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Navigation("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
22
FabWorks.Core/Models/BomItem.cs
Normal file
22
FabWorks.Core/Models/BomItem.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
33
FabWorks.Core/Models/CutTemplate.cs
Normal file
33
FabWorks.Core/Models/CutTemplate.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
14
FabWorks.Core/Models/Drawing.cs
Normal file
14
FabWorks.Core/Models/Drawing.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FabWorks.Core.Models
|
||||
{
|
||||
public class Drawing
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string DrawingNumber { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string PdfContentHash { get; set; }
|
||||
|
||||
public virtual ICollection<ExportRecord> ExportRecords { get; set; } = new List<ExportRecord>();
|
||||
}
|
||||
}
|
||||
25
FabWorks.Core/Models/ExportRecord.cs
Normal file
25
FabWorks.Core/Models/ExportRecord.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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 int? DrawingId { get; set; }
|
||||
public int? DrawingRevision { get; set; }
|
||||
public virtual Drawing Drawing { get; set; }
|
||||
|
||||
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
|
||||
}
|
||||
}
|
||||
20
FabWorks.Core/Models/FormProgram.cs
Normal file
20
FabWorks.Core/Models/FormProgram.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
171
FabWorks.Core/PressBrake/Extensions.cs
Normal file
171
FabWorks.Core/PressBrake/Extensions.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
FabWorks.Core/PressBrake/MatType.cs
Normal file
11
FabWorks.Core/PressBrake/MatType.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace FabWorks.Core.PressBrake
|
||||
{
|
||||
public enum MatType
|
||||
{
|
||||
MildSteel,
|
||||
HighStrengthSteel,
|
||||
Stainless,
|
||||
SoftAluminum,
|
||||
HardAluminum
|
||||
}
|
||||
}
|
||||
59
FabWorks.Core/PressBrake/Program.cs
Normal file
59
FabWorks.Core/PressBrake/Program.cs
Normal file
@@ -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<ToolSetup>();
|
||||
LowerToolSets = new List<ToolSetup>();
|
||||
Steps = new List<Step>();
|
||||
}
|
||||
|
||||
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<ToolSetup> UpperToolSets { get; set; }
|
||||
|
||||
public List<ToolSetup> LowerToolSets { get; set; }
|
||||
|
||||
public List<Step> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
149
FabWorks.Core/PressBrake/ProgramReader.cs
Normal file
149
FabWorks.Core/PressBrake/ProgramReader.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
FabWorks.Core/PressBrake/SegEntry.cs
Normal file
7
FabWorks.Core/PressBrake/SegEntry.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace FabWorks.Core.PressBrake
|
||||
{
|
||||
public class SegEntry
|
||||
{
|
||||
public double SegValue { get; set; }
|
||||
}
|
||||
}
|
||||
36
FabWorks.Core/PressBrake/Step.cs
Normal file
36
FabWorks.Core/PressBrake/Step.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
24
FabWorks.Core/PressBrake/ToolSetup.cs
Normal file
24
FabWorks.Core/PressBrake/ToolSetup.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FabWorks.Core.PressBrake
|
||||
{
|
||||
public class ToolSetup
|
||||
{
|
||||
public ToolSetup()
|
||||
{
|
||||
Segments = new List<SegEntry>();
|
||||
}
|
||||
|
||||
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<SegEntry> Segments { get; set; }
|
||||
}
|
||||
}
|
||||
32
FabWorks.Tests/FabWorks.Tests.csproj
Normal file
32
FabWorks.Tests/FabWorks.Tests.csproj
Normal file
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FabWorks.Core\FabWorks.Core.csproj" />
|
||||
<ProjectReference Include="..\FabWorks.Api\FabWorks.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="TestData\**" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
53
FabWorks.Tests/FormProgramServiceTests.cs
Normal file
53
FabWorks.Tests/FormProgramServiceTests.cs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
48
FabWorks.Tests/PressBrake/ProgramReaderTests.cs
Normal file
48
FabWorks.Tests/PressBrake/ProgramReaderTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
593
FabWorks.Tests/TestData/sample.pgm
Normal file
593
FabWorks.Tests/TestData/sample.pgm
Normal file
@@ -0,0 +1,593 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Document>
|
||||
<PressBrakeProgram Version="13" ProgName="" TeachName="" PartName="C:\Users\aj.REMCO\Desktop\4980 A05-1 PT02.part" SetupNotes="" ProgNotes="" MatThick="0.06" MatType="2" KFactor="0.42" RZEnabled="1" FEnabled="0" KFactorAuto="0" PartsBetween="0" DryRun="0" LeftFingerType="2" RightFingerType="2" HasPart="1" CBAngMode="1" CBVee="0.75" CBMute="20.752" CBClamp="20.492" CBDieAng="85" CBTopOfDie="20.442" ToolSelLock="0" NumSteps="8" GageType="6">
|
||||
<UpperToolSets>
|
||||
<ToolList Count="1">
|
||||
<ToolSetup Name="50210 with double riser" ID="1" Length="63" NumSegs="4">
|
||||
<SegStackup>
|
||||
<SegEntry SegValue="36"/>
|
||||
<SegEntry SegValue="18"/>
|
||||
<SegEntry SegValue="8"/>
|
||||
<SegEntry SegValue="1"/>
|
||||
</SegStackup>
|
||||
</ToolSetup>
|
||||
</ToolList>
|
||||
</UpperToolSets>
|
||||
<LowerToolSets>
|
||||
<ToolList Count="1">
|
||||
<ToolSetup Name="0.750 x 85 x 144" ID="1" Length="144"/>
|
||||
</ToolList>
|
||||
</LowerToolSets>
|
||||
<StepData>
|
||||
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.0859" StrokeLen="1.7399" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-15.2429" ZRight="15.2429" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" CBTopStop="22.1819" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.5429" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
|
||||
<UIDSelectList Size="1">
|
||||
<UID IntValue="1"/>
|
||||
</UIDSelectList>
|
||||
<LIDSelectList Size="1">
|
||||
<LID IntValue="1"/>
|
||||
</LIDSelectList>
|
||||
<ProgOutputs>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
</ProgOutputs>
|
||||
</Step>
|
||||
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.7179" StrokeLen="8.8696" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="1" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-16.7954" ZRight="0.3125" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.859" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
|
||||
<UIDSelectList Size="1">
|
||||
<UID IntValue="1"/>
|
||||
</UIDSelectList>
|
||||
<LIDSelectList Size="1">
|
||||
<LID IntValue="1"/>
|
||||
</LIDSelectList>
|
||||
<ProgOutputs>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
</ProgOutputs>
|
||||
</Step>
|
||||
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.0859" StrokeLen="1.7399" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-15.2429" ZRight="15.2429" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.1819" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.5429" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
|
||||
<UIDSelectList Size="1">
|
||||
<UID IntValue="1"/>
|
||||
</UIDSelectList>
|
||||
<LIDSelectList Size="1">
|
||||
<LID IntValue="1"/>
|
||||
</LIDSelectList>
|
||||
<ProgOutputs>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
</ProgOutputs>
|
||||
</Step>
|
||||
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.7179" StrokeLen="8.8696" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="1" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-0.3125" ZRight="16.7954" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.859" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
|
||||
<UIDSelectList Size="1">
|
||||
<UID IntValue="1"/>
|
||||
</UIDSelectList>
|
||||
<LIDSelectList Size="1">
|
||||
<LID IntValue="1"/>
|
||||
</LIDSelectList>
|
||||
<ProgOutputs>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
</ProgOutputs>
|
||||
</Step>
|
||||
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4609" StrokeLen="1.74" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-29.1179" ZRight="29.1179" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.182" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.7304" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
|
||||
<UIDSelectList Size="1">
|
||||
<UID IntValue="1"/>
|
||||
</UIDSelectList>
|
||||
<LIDSelectList Size="1">
|
||||
<LID IntValue="1"/>
|
||||
</LIDSelectList>
|
||||
<ProgOutputs>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
</ProgOutputs>
|
||||
</Step>
|
||||
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4679" StrokeLen="8.8759" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-30.6705" ZRight="30.6704" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.734" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
|
||||
<UIDSelectList Size="1">
|
||||
<UID IntValue="1"/>
|
||||
</UIDSelectList>
|
||||
<LIDSelectList Size="1">
|
||||
<LID IntValue="1"/>
|
||||
</LIDSelectList>
|
||||
<ProgOutputs>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
</ProgOutputs>
|
||||
</Step>
|
||||
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4609" StrokeLen="1.74" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-29.1179" ZRight="29.1179" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.182" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.7304" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
|
||||
<UIDSelectList Size="1">
|
||||
<UID IntValue="1"/>
|
||||
</UIDSelectList>
|
||||
<LIDSelectList Size="1">
|
||||
<LID IntValue="1"/>
|
||||
</LIDSelectList>
|
||||
<ProgOutputs>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
</ProgOutputs>
|
||||
</Step>
|
||||
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4679" StrokeLen="8.8759" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-30.6704" ZRight="30.6704" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.734" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
|
||||
<UIDSelectList Size="1">
|
||||
<UID IntValue="1"/>
|
||||
</UIDSelectList>
|
||||
<LIDSelectList Size="1">
|
||||
<LID IntValue="1"/>
|
||||
</LIDSelectList>
|
||||
<ProgOutputs>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
<ProgOutData/>
|
||||
</ProgOutputs>
|
||||
</Step>
|
||||
</StepData>
|
||||
</PressBrakeProgram>
|
||||
<PressBrakePart Version="2" FlangeCount="9" MatType="2" KFactor="0.42" KFactorAuto="0" FingerSelPref="3">
|
||||
<Flanges>
|
||||
<Flange Version="5" FlangeID="1" FlangeDim="63.3409" FlangeWidth="1.554956" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0" IntBend="0">
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="0"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="0"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="0"/>
|
||||
</Quaternion>
|
||||
<BendList>
|
||||
<Bend Version="9" BendLength="63.460859" BendRadius="0.125" BendSeq="5" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="1" EndFlangeID="2" Rotate180="1" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="5" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="0.117968" YVal="31.73043" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="-0">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="1"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="0.000001" YVal="31.73043" ZVal="0"/>
|
||||
</Bend>
|
||||
</BendList>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="6" OrgSegListCount="6">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="0.117972" YVal="0.059985" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.122918" YVal="0.0625" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="1.672918" YVal="1.6125" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="1.672918" YVal="61.848358" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.122918" YVal="63.398358" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.117961" YVal="63.400878" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="0.000003" YVal="0" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122918" YVal="0.0625" ZVal="0.03"/>
|
||||
<Segment3D XVal="1.672918" YVal="1.6125" ZVal="0.03"/>
|
||||
<Segment3D XVal="1.672918" YVal="61.848358" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122918" YVal="63.398358" ZVal="0.03"/>
|
||||
<Segment3D XVal="0" YVal="63.460859" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
<Flange Version="5" FlangeID="2" FlangeDim="63.5859" FlangeWidth="8.1099" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
|
||||
<Quaternion Version="1" SVal="-0">
|
||||
<Segment3D XVal="0.707107" YVal="0" ZVal="0.707107"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="1"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0">
|
||||
<Segment3D XVal="0.707107" YVal="0" ZVal="0.707107"/>
|
||||
</Quaternion>
|
||||
<BendList>
|
||||
<Bend Version="9" BendLength="63.467927" BendRadius="0.125" BendSeq="6" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="2" EndFlangeID="3" Rotate180="1" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="6" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="8.109896" YVal="0" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="8.34583" YVal="0" ZVal="0"/>
|
||||
</Bend>
|
||||
</BendList>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="8" OrgSegListCount="8">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="-0.000001" YVal="-31.790412" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109896" YVal="-31.790554" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109899" YVal="31.790554" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104945" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004945" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="31.790414" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="0.000003" YVal="-31.73043" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122915" YVal="-31.792928" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222915" YVal="-31.792928" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.345832" YVal="-31.733962" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.345827" YVal="31.733966" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222912" YVal="31.79293" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122912" YVal="31.79293" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0.000003" YVal="31.73043" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
<Flange Version="5" FlangeID="3" FlangeDim="63.3599" FlangeWidth="35.609897" Editable="0" BendCount="3" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
|
||||
<Quaternion Version="1" SVal="0">
|
||||
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="1"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="0">
|
||||
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<BendList>
|
||||
<Bend Version="9" BendLength="63.467928" BendRadius="0.125" BendSeq="8" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="4" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="8" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="35.609893" YVal="-0.000003" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="35.845827" YVal="-0.000003" ZVal="0"/>
|
||||
</Bend>
|
||||
<Bend Version="9" BendLength="35.717928" BendRadius="0.125" BendSeq="2" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="5" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="2" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="17.804945" YVal="-31.679949" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="-0.707107">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="17.922911" YVal="-31.797916" ZVal="0"/>
|
||||
</Bend>
|
||||
<Bend Version="9" BendLength="35.717928" BendRadius="0.125" BendSeq="4" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="6" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="4" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="17.804948" YVal="31.679946" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="-0.707107">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="17.922915" YVal="31.797913" ZVal="0"/>
|
||||
</Bend>
|
||||
</BendList>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="-0.000001" YVal="-31.677374" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004945" YVal="-31.675001" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.00257" YVal="-31.679952" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="35.607317" YVal="-31.679947" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="35.604945" YVal="-31.675002" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="35.609896" YVal="-31.677377" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="35.609893" YVal="31.67737" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="35.604948" YVal="31.674998" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="35.607323" YVal="31.679949" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.002576" YVal="31.679944" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004948" YVal="31.674999" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="31.677374" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="0" YVal="-31.733964" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122912" YVal="-31.675001" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.063947" YVal="-31.797916" ZVal="0.03"/>
|
||||
<Segment3D XVal="35.781875" YVal="-31.797916" ZVal="0.03"/>
|
||||
<Segment3D XVal="35.722912" YVal="-31.675002" ZVal="0.03"/>
|
||||
<Segment3D XVal="35.845827" YVal="-31.733967" ZVal="0.03"/>
|
||||
<Segment3D XVal="35.845827" YVal="31.733961" ZVal="0.03"/>
|
||||
<Segment3D XVal="35.722915" YVal="31.674998" ZVal="0.03"/>
|
||||
<Segment3D XVal="35.781879" YVal="31.797912" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.063951" YVal="31.797913" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122915" YVal="31.674999" ZVal="0.03"/>
|
||||
<Segment3D XVal="0" YVal="31.733964" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
<Flange Version="5" FlangeID="4" FlangeDim="63.5859" FlangeWidth="8.109903" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
|
||||
<Quaternion Version="1" SVal="0">
|
||||
<Segment3D XVal="0.707107" YVal="0" ZVal="-0.707107"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="1"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="0">
|
||||
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<BendList>
|
||||
<Bend Version="9" BendLength="63.460859" BendRadius="0.125" BendSeq="7" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="4" EndFlangeID="7" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="7" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="8.109896" YVal="0.000001" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="-1">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="8.34583" YVal="0.000001" ZVal="0"/>
|
||||
</Bend>
|
||||
</BendList>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="8" OrgSegListCount="8">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="-0.000001" YVal="-31.790554" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104948" YVal="-31.792929" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109902" YVal="-31.79041" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109893" YVal="31.790416" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104951" YVal="31.792929" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004951" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="31.790554" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="0" YVal="-31.733964" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122915" YVal="-31.792928" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222915" YVal="-31.792929" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.34583" YVal="-31.730429" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.34583" YVal="31.730431" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222918" YVal="31.792929" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122918" YVal="31.79293" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0" YVal="31.733964" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
<Flange Version="5" FlangeID="5" FlangeDim="35.8359" FlangeWidth="8.109898" Editable="0" BendCount="2" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
|
||||
<Quaternion Version="1" SVal="-0.5">
|
||||
<Segment3D XVal="-0.5" YVal="-0.5" ZVal="0.5"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0.707107">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0.5">
|
||||
<Segment3D XVal="-0.5" YVal="-0.5" ZVal="0.5"/>
|
||||
</Quaternion>
|
||||
<BendList>
|
||||
<Bend Version="9" BendLength="19.16793" BendRadius="0.125" BendSeq="1" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="5" EndFlangeID="8" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="1" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="8.109897" YVal="8.271464" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="8.345831" YVal="8.271464" ZVal="0"/>
|
||||
</Bend>
|
||||
<Bend Version="9" BendLength="15.917929" BendRadius="0.125" BendSeq="1" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="5" EndFlangeID="8" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="1" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="8.109895" YVal="-9.896465" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="8.345829" YVal="-9.896465" ZVal="0"/>
|
||||
</Bend>
|
||||
</BendList>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="-0.000001" YVal="-17.915554" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.00495" YVal="-17.917929" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.10495" YVal="-17.917928" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109895" YVal="-17.915413" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109896" YVal="-1.937499" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104949" YVal="-1.937499" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104949" YVal="-1.312499" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109898" YVal="-1.312499" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109898" YVal="17.915413" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104948" YVal="17.91793" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004948" YVal="17.917929" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="17.915555" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="0.000002" YVal="-17.858964" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122916" YVal="-17.917929" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222916" YVal="-17.917928" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.345831" YVal="-17.855428" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.34583" YVal="-1.937499" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222916" YVal="-1.937499" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222916" YVal="-1.312499" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.345831" YVal="-1.312499" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.345828" YVal="17.85543" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222915" YVal="17.91793" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122915" YVal="17.917929" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0.000002" YVal="17.858964" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
<Flange Version="5" FlangeID="6" FlangeDim="35.8359" FlangeWidth="8.109899" Editable="0" BendCount="2" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
|
||||
<Quaternion Version="1" SVal="0.5">
|
||||
<Segment3D XVal="-0.5" YVal="0.5" ZVal="0.5"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="0.707107">
|
||||
<Segment3D XVal="-0" YVal="0" ZVal="-0.707107"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="0.5">
|
||||
<Segment3D XVal="-0.5" YVal="0.5" ZVal="0.5"/>
|
||||
</Quaternion>
|
||||
<BendList>
|
||||
<Bend Version="9" BendLength="19.167929" BendRadius="0.125" BendSeq="3" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="6" EndFlangeID="9" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="3" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="8.109896" YVal="-8.271465" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="8.345829" YVal="-8.271465" ZVal="0"/>
|
||||
</Bend>
|
||||
<Bend Version="9" BendLength="15.917929" BendRadius="0.125" BendSeq="3" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="6" EndFlangeID="9" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="3" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
|
||||
<Segment3D XVal="8.109897" YVal="9.896464" ZVal="0"/>
|
||||
<Quaternion Version="1" SVal="1">
|
||||
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
|
||||
</Quaternion>
|
||||
<Segment3D XVal="8.345831" YVal="9.896464" ZVal="0"/>
|
||||
</Bend>
|
||||
</BendList>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="-0.000001" YVal="-17.915554" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.00495" YVal="-17.917929" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.10495" YVal="-17.917928" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109895" YVal="-17.915413" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109897" YVal="1.312501" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104949" YVal="1.312501" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104949" YVal="1.937501" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109898" YVal="1.937501" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.109898" YVal="17.915413" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="8.104948" YVal="17.91793" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004948" YVal="17.917929" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="17.915555" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="0.000002" YVal="-17.858964" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122916" YVal="-17.917929" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222916" YVal="-17.917928" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.345831" YVal="-17.855428" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.34583" YVal="1.312501" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222915" YVal="1.312501" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222915" YVal="1.937501" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.34583" YVal="1.937501" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.345828" YVal="17.85543" ZVal="0.03"/>
|
||||
<Segment3D XVal="8.222915" YVal="17.91793" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122915" YVal="17.917929" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0.000002" YVal="17.858964" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
<Flange Version="5" FlangeID="7" FlangeDim="63.3409" FlangeWidth="1.554952" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
|
||||
<Quaternion Version="1" SVal="-0">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="1"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="0">
|
||||
<Segment3D XVal="-0" YVal="0" ZVal="-1"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0">
|
||||
<Segment3D XVal="-1" YVal="-0" ZVal="0"/>
|
||||
</Quaternion>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="6" OrgSegListCount="6">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="-0.000001" YVal="-31.670448" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004951" YVal="-31.66793" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="1.554951" YVal="-30.11793" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="1.554948" YVal="30.117928" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004948" YVal="31.667928" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="31.670445" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="0.000003" YVal="-31.73043" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122918" YVal="-31.66793" ZVal="0.03"/>
|
||||
<Segment3D XVal="1.672918" YVal="-30.11793" ZVal="0.03"/>
|
||||
<Segment3D XVal="1.672915" YVal="30.117928" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122915" YVal="31.667928" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0.000003" YVal="31.73043" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
<Flange Version="5" FlangeID="8" FlangeDim="35.5909" FlangeWidth="1.554951" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
|
||||
<Quaternion Version="1" SVal="-0.707107">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0.707107">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="-0.707107">
|
||||
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
|
||||
</Quaternion>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="10" OrgSegListCount="10">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="-0" YVal="-9.583965" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.554948" YVal="-9.583965" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.554948" YVal="-10.208965" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0" YVal="-10.208965" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="-26.06691" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004947" YVal="-26.064394" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="1.554947" YVal="-24.514394" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="1.554949" YVal="7.971464" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.00495" YVal="9.521464" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0" YVal="9.523981" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="0" YVal="-9.583965" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.672915" YVal="-9.583965" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.672915" YVal="-10.208965" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0" YVal="-10.208965" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="-26.126894" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122913" YVal="-26.064394" ZVal="0.03"/>
|
||||
<Segment3D XVal="1.672914" YVal="-24.514394" ZVal="0.03"/>
|
||||
<Segment3D XVal="1.672916" YVal="7.971464" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122916" YVal="9.521464" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0" YVal="9.583965" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
<Flange Version="5" FlangeID="9" FlangeDim="35.5909" FlangeWidth="1.554949" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
|
||||
<Quaternion Version="1" SVal="0.707107">
|
||||
<Segment3D XVal="-0" YVal="-0" ZVal="0.707107"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="0.707107">
|
||||
<Segment3D XVal="-0" YVal="0" ZVal="-0.707107"/>
|
||||
</Quaternion>
|
||||
<Quaternion Version="1" SVal="0.707107">
|
||||
<Segment3D XVal="-0" YVal="-0" ZVal="0.707107"/>
|
||||
</Quaternion>
|
||||
<TopList>
|
||||
<Feature3D Version="6" SegCount="10" OrgSegListCount="10">
|
||||
<SegmentList>
|
||||
<Segment3D XVal="-0" YVal="-9.523981" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004948" YVal="-9.521464" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="1.554948" YVal="-7.971464" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="1.554948" YVal="24.514393" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.004948" YVal="26.064393" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="26.06691" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0" YVal="10.208964" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.554948" YVal="10.208964" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="0.554948" YVal="9.583964" ZVal="0.03" ZCent="0.03"/>
|
||||
<Segment3D XVal="-0" YVal="9.583964" ZVal="0.03" ZCent="0.03"/>
|
||||
</SegmentList>
|
||||
<OrgSegList>
|
||||
<Segment3D XVal="-0" YVal="-9.583964" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122915" YVal="-9.521464" ZVal="0.03"/>
|
||||
<Segment3D XVal="1.672915" YVal="-7.971464" ZVal="0.03"/>
|
||||
<Segment3D XVal="1.672915" YVal="24.514393" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.122915" YVal="26.064393" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0.000001" YVal="26.126894" ZVal="0.03"/>
|
||||
<Segment3D XVal="0" YVal="10.208964" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.672915" YVal="10.208964" ZVal="0.03"/>
|
||||
<Segment3D XVal="0.672915" YVal="9.583964" ZVal="0.03"/>
|
||||
<Segment3D XVal="-0" YVal="9.583964" ZVal="0.03"/>
|
||||
</OrgSegList>
|
||||
</Feature3D>
|
||||
</TopList>
|
||||
</Flange>
|
||||
</Flanges>
|
||||
</PressBrakePart>
|
||||
</Document>
|
||||
78
README.md
78
README.md
@@ -1,84 +1,62 @@
|
||||
# ExportDXF
|
||||
|
||||
A Windows desktop application that automates exporting flat pattern DXF files from SolidWorks drawings, assemblies, and parts. Built for sheet metal fabrication workflows, it extracts BOM data, generates DXF flat patterns with etch/bend line markings, and exports everything to a local folder with an Excel workbook for downstream tools like [OpenNest](https://github.com/ajisaacs/OpenNest).
|
||||
A Windows desktop application that automates exporting flat pattern DXF files from SolidWorks drawings, assemblies, and parts. Built for sheet metal fabrication workflows, it extracts BOM data, generates DXF flat patterns with etch/bend line markings, and exports drawing PDFs to a local output folder with SQL Server tracking.
|
||||
|
||||
## Features
|
||||
|
||||
- **Batch DXF export** from SolidWorks drawings, assemblies, or individual parts
|
||||
- **Flat pattern generation** with automatic sheet metal detection
|
||||
- **Etch line insertion** on bend-up lines for fabrication reference (via EtchBendLines/ACadSharp)
|
||||
- **Etch line insertion** on bend-up lines for fabrication reference (via EtchBendLines library)
|
||||
- **PDF export** of SolidWorks drawings
|
||||
- **Excel BOM output** -- generates an xlsx workbook with a BOM sheet (direct copy of the SolidWorks BOM table) and a Cut Templates sheet (DXF filenames, thicknesses, K-factors, bend radii, content hashes)
|
||||
- **Revision tracking** -- content hashing detects unchanged DXFs across re-exports; changed files get revision suffixes (e.g., `PT03 Rev2.dxf`)
|
||||
- **Configurable filename template** -- format like `4321 A01 PT{item_no:2}` with placeholders for item number, part name, configuration, and material
|
||||
- **Pluggable drawing info extraction** -- auto-fills the filename template from the document name; extensible via `IDrawingInfoExtractor`
|
||||
- **Local file export** -- saves DXFs and PDFs to a configurable output folder
|
||||
- **SQL Server database** -- tracks export history and BOM data via Entity Framework Core
|
||||
- **View flip control** with automatic, manual, and prefer-up strategies
|
||||
- **Active document tracking** -- connects to SolidWorks and updates the UI when the active document changes
|
||||
- **Per-export and app-level logging** to plain text log files
|
||||
- **Active document tracking** -- connects to SolidWorks and displays the active document in the title bar
|
||||
- **BOM extraction** from drawing BOM tables or assembly component trees
|
||||
- **Drawing history dropdowns** -- equipment and drawing number filters populated from past exports
|
||||
|
||||
## Requirements
|
||||
|
||||
- Windows 10/11
|
||||
- .NET 8.0
|
||||
- SolidWorks (installed and licensed)
|
||||
- SQL Server (default: `localhost`, database: `ExportDxfDb`, Windows auth)
|
||||
|
||||
## Solution Structure
|
||||
|
||||
```
|
||||
ExportDXF.sln
|
||||
ExportDXF/ Main WinForms application
|
||||
EtchBendLines/ Library for adding etch lines to DXF files (git submodule, uses ACadSharp)
|
||||
EtchBendLines/ Library for adding etch lines to DXF files (git submodule)
|
||||
netDxf/ DXF file read/write library
|
||||
```
|
||||
|
||||
### Key Namespaces
|
||||
|
||||
| Namespace | Purpose |
|
||||
|-----------|---------|
|
||||
| `ExportDXF.Services` | Core services -- SolidWorks connection, DXF export, BOM extraction, PDF export, Excel output, logging, filename template parsing, drawing info extraction |
|
||||
| `ExportDXF.Forms` | WinForms UI -- main export form with log, BOM, and cut templates grids |
|
||||
| `ExportDXF.Models` | Data models -- BomItem, CutTemplate, ExportContext, SolidWorksDocument |
|
||||
| `ExportDXF.Services` | Core services -- SolidWorks connection, DXF export, BOM extraction, PDF export, file export |
|
||||
| `ExportDXF.Forms` | WinForms UI -- main export form with log and BOM grids |
|
||||
| `ExportDXF.Models` | Data models -- BomItem, ExportRecord, ExportContext, SolidWorksDocument |
|
||||
| `ExportDXF.Data` | Entity Framework Core DbContext for SQL Server persistence |
|
||||
| `ExportDXF.ViewFlipDeciders` | Strategies for determining if a flat pattern view should be flipped |
|
||||
| `ExportDXF.ItemExtractors` | Extract component items from BOM tables and assemblies |
|
||||
| `ExportDXF.Utilities` | SolidWorks helpers, content hashing, sheet metal property extraction, text utilities |
|
||||
| `ExportDXF.Utilities` | SolidWorks helpers, sheet metal property extraction, text utilities |
|
||||
| `ExportDXF.Extensions` | Extension methods for SolidWorks, UI, unit conversion, strings, TimeSpan |
|
||||
| `EtchBendLines` | Post-processes DXF files to add etch marks on bend-up lines |
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Startup** -- Connects to a running SolidWorks instance (or launches one). Auto-fills the filename template from the active document name using pluggable extractors.
|
||||
1. **Startup** -- Connects to a running SolidWorks instance (or launches one) asynchronously. Loads past export history from the database to populate equipment/drawing dropdowns. Displays the active SolidWorks document name in the title bar.
|
||||
|
||||
2. **Export** -- Based on the active document type:
|
||||
- **Drawing**: Copies the raw BOM table to the Excel BOM sheet, exports the drawing as PDF, then generates flat pattern DXFs for each sheet metal component.
|
||||
- **Drawing**: Extracts BOM items from BOM tables, exports the drawing as PDF, then iterates through each component to generate flat pattern DXFs.
|
||||
- **Assembly**: Extracts components from the assembly tree and generates flat pattern DXFs for each sheet metal part.
|
||||
- **Part**: Generates a single flat pattern DXF for the active part.
|
||||
|
||||
3. **Post-processing** -- Each exported DXF is run through the EtchBendLines library, which identifies bend-up lines and adds short etch marks at their endpoints on a dedicated `ETCH` layer. Only up bends are etched because SolidWorks automatically flips the flat pattern so the shortest flange is the first bend. The etch marks let the press brake operator verify part orientation right off the laser table without flipping.
|
||||
3. **Post-processing** -- Each exported DXF is run through the EtchBendLines library, which identifies bend-up lines and adds short etch marks at their endpoints on a dedicated `ETCH` layer. Only up bends are etched because SolidWorks automatically flips the flat pattern so the shortest flange is the first bend. The etch marks let the press brake operator verify part orientation right off the laser table without flipping -- the first bend (shortest flange) will always have an etch line.
|
||||
|
||||
4. **Save** -- DXF and PDF files are saved to a `Templates/` folder next to the source file. An Excel workbook is written with the BOM and Cut Templates sheets. If re-exporting, content hashes are compared against the existing workbook -- unchanged DXFs are skipped, changed DXFs get a revision suffix.
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
C:\Projects\4321\
|
||||
├── 4321 A01.SLDDRW
|
||||
└── Templates\
|
||||
├── 4321 A01 PT01.dxf
|
||||
├── 4321 A01 PT02.dxf
|
||||
├── 4321 A01 PT03.dxf
|
||||
├── 4321 A01 PT03 Rev2.dxf (revised, original kept)
|
||||
├── 4321 A01.pdf
|
||||
├── 4321 A01.xlsx
|
||||
└── 4321 A01.log
|
||||
```
|
||||
|
||||
### Excel Workbook
|
||||
|
||||
**BOM sheet** -- exact copy of all visible columns and rows from the SolidWorks BOM table (Drawing exports only).
|
||||
|
||||
**Cut Templates sheet**:
|
||||
|
||||
| Item # | File Name | Revision | Thickness | K-Factor | Bend Radius | Content Hash |
|
||||
|--------|-----------|----------|-----------|----------|-------------|--------------|
|
||||
4. **Save** -- DXF and PDF files are copied to the configured output folder. Export records and BOM items (with sheet metal properties) are saved to the SQL Server database.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -87,18 +65,20 @@ Settings are in `App.config`:
|
||||
```xml
|
||||
<appSettings>
|
||||
<add key="MaxBendRadius" value="2.0" />
|
||||
<add key="DefaultSuffix" value="PT{item_no:2}" />
|
||||
<add key="ExportOutputFolder" value="C:\ExportDXF\Output" />
|
||||
</appSettings>
|
||||
<connectionStrings>
|
||||
<add name="ExportDxfDb"
|
||||
connectionString="Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||
providerName="Microsoft.Data.SqlClient" />
|
||||
</connectionStrings>
|
||||
```
|
||||
|
||||
### Filename Template Placeholders
|
||||
## DXF Filename Format
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
|-------------|-------------|---------|
|
||||
| `{item_no:N}` | Item number, zero-padded to N digits | `{item_no:2}` → `03` |
|
||||
| `{part_name}` | SolidWorks part name | `Bracket` |
|
||||
| `{config}` | Configuration name | `Default` |
|
||||
| `{material}` | Material name | `AISI 304` |
|
||||
Exported files follow the pattern: `{DrawingNumber} PT{ItemNo}.dxf`
|
||||
|
||||
Example: `5007 A02 PT01.dxf`
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
32
docs/plans/2026-02-17-autofill-from-export-history-design.md
Normal file
32
docs/plans/2026-02-17-autofill-from-export-history-design.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Auto-fill Equipment/Drawing from Export History
|
||||
|
||||
## Problem
|
||||
|
||||
When a SolidWorks file is opened that doesn't match the `DrawingInfo` regex (e.g., `Conveyor Frame.sldasm` instead of `5028 A02 Conveyor.slddrw`), the equipment and drawing number dropdowns are left empty even though the file may have been exported before with known values.
|
||||
|
||||
## Decision
|
||||
|
||||
- **Lookup key:** SolidWorks source file path (`ActiveDocument.FilePath`)
|
||||
- **Storage:** Query the existing `ExportRecords` table (no new table or migration)
|
||||
- **Priority:** Database lookup first; fall back to title regex parse if no history found
|
||||
|
||||
## Design
|
||||
|
||||
### Modify `UpdateActiveDocumentDisplay()` in `MainForm.cs`
|
||||
|
||||
When the active document changes:
|
||||
|
||||
1. Query the DB for the most recent `ExportRecord` where `SourceFilePath` matches `activeDoc.FilePath` (case-insensitive)
|
||||
2. If found, parse the stored `DrawingNumber` via `DrawingInfo.Parse()` and auto-fill equipment/drawing dropdowns
|
||||
3. If not found, fall back to current behavior: `DrawingInfo.Parse(activeDoc.Title)`
|
||||
|
||||
### What doesn't change
|
||||
|
||||
- No schema changes, no new migration
|
||||
- Equipment/drawing dropdowns still populated with historical values in `InitializeDrawingDropdowns()`
|
||||
- Export flow untouched
|
||||
|
||||
### Error handling
|
||||
|
||||
- DB query wrapped in try/catch so failures don't break the UI
|
||||
- Case-insensitive path comparison (Windows paths are case-insensitive)
|
||||
1191
docs/plans/2026-02-17-fabworks-api.md
Normal file
1191
docs/plans/2026-02-17-fabworks-api.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user