Compare commits
47 Commits
master
...
feature/fa
| Author | SHA1 | Date | |
|---|---|---|---|
| bd3e7c2a36 | |||
| b9e84de7c0 | |||
| f6cd91f1b5 | |||
| 3554bb6110 | |||
| 4707e96359 | |||
| c5bd7fb4c8 | |||
| 13c61a82a4 | |||
| 444a077cbc | |||
| c4920f933d | |||
| b472729fda | |||
| 5d2948d563 | |||
| 71c65e0bf5 | |||
| 53aa23f762 | |||
| 036ab2a55a | |||
| f9e7ace35d | |||
| 622cbf1170 | |||
| 4a3f33db33 | |||
| 77d0157370 | |||
| 26e9233b30 | |||
| e59584a5c0 | |||
| dcc508d479 | |||
| 1266378b51 | |||
| 5de40ebafd | |||
| e072919a59 | |||
| 7db44640ca | |||
| 0d5742124e | |||
| 463916c75c | |||
| c06d834e05 | |||
| d3c154b875 | |||
| 2721c33a39 | |||
| 5ec66f9039 | |||
| cf76ca8bb1 | |||
| 696bf2f72c | |||
| 8de441e126 | |||
| 8b6950ef28 | |||
| dba68ecc71 | |||
| f75b83d483 | |||
| 2273a83e42 | |||
| e10a7ed0ed | |||
| 16dc74c35d | |||
| 9e5e44c1ed | |||
| ab76fa61c9 | |||
| 28c9f715be | |||
| 2bef75f548 | |||
| 78a8a2197d | |||
| 719dca1ca5 | |||
| a17d8cac49 |
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -245,3 +245,6 @@ ModelManifest.xml
|
||||
|
||||
# Test documents
|
||||
TestDocs/
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/
|
||||
|
||||
Submodule EtchBendLines updated: 89d987f6c6...da4d3228b0
@@ -1,32 +1,88 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29123.88
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.3.11512.155 d18.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportDXF", "ExportDXF\ExportDXF.csproj", "{05F21D73-FD31-4E77-8D9B-41C86D4D8305}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLines\EtchBendLines\EtchBendLines.csproj", "{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "netDxf", "EtchBendLines\netDxf\netDxf\netDxf.csproj", "{785380E0-CEB9-4C34-82E5-60D0E33E848E}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Core", "FabWorks.Core\FabWorks.Core.csproj", "{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Tests", "FabWorks.Tests\FabWorks.Tests.csproj", "{6DD89774-D86B-47E9-B982-2794BD95616A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Api", "FabWorks.Api\FabWorks.Api.csproj", "{9BD571FA-52D8-430D-8843-FEB6EABD421C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x64.Build.0 = Release|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x86.Build.0 = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.Build.0 = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.Build.0 = Release|Any CPU
|
||||
{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,10 +1,12 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ExportDXF
|
||||
{
|
||||
public class DrawingInfo
|
||||
{
|
||||
private static Regex drawingFormatRegex = new Regex(@"(?<equipmentNo>[345]\d{3}(-\d+\w{1,2})?)\s?(?<dwgNo>[ABEP]\d+(-?(\d+[A-Z]?))?)", RegexOptions.IgnoreCase);
|
||||
private static Regex equipmentOnlyRegex = new Regex(@"^(?<equipmentNo>[345]\d{3}(-\d+\w{1,2})?)\b", RegexOptions.IgnoreCase);
|
||||
|
||||
public string EquipmentNo { get; set; }
|
||||
|
||||
@@ -12,8 +14,39 @@ 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))
|
||||
return EquipmentNo ?? string.Empty;
|
||||
return $"{EquipmentNo} {DrawingNo}";
|
||||
}
|
||||
|
||||
@@ -35,7 +68,21 @@ namespace ExportDXF
|
||||
var match = drawingFormatRegex.Match(input);
|
||||
|
||||
if (match.Success == false)
|
||||
{
|
||||
// Try matching just the equipment number (e.g. "5028 Prox switch bracket")
|
||||
var eqMatch = equipmentOnlyRegex.Match(input);
|
||||
if (eqMatch.Success)
|
||||
{
|
||||
return new DrawingInfo
|
||||
{
|
||||
EquipmentNo = eqMatch.Groups["equipmentNo"].Value,
|
||||
DrawingNo = null,
|
||||
Source = input
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var dwg = new DrawingInfo();
|
||||
|
||||
@@ -46,4 +93,4 @@ namespace ExportDXF
|
||||
return dwg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<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>
|
||||
|
||||
|
||||
58
ExportDXF/Forms/MainForm.Designer.cs
generated
58
ExportDXF/Forms/MainForm.Designer.cs
generated
@@ -42,6 +42,8 @@ namespace ExportDXF.Forms
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
label2 = new System.Windows.Forms.Label();
|
||||
drawingNoBox = new System.Windows.Forms.ComboBox();
|
||||
titleLabel = new System.Windows.Forms.Label();
|
||||
titleBox = new System.Windows.Forms.TextBox();
|
||||
mainTabControl.SuspendLayout();
|
||||
logEventsTab.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).BeginInit();
|
||||
@@ -54,10 +56,10 @@ namespace ExportDXF.Forms
|
||||
// runButton
|
||||
//
|
||||
runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
runButton.Location = new System.Drawing.Point(514, 13);
|
||||
runButton.Location = new System.Drawing.Point(508, 12);
|
||||
runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
runButton.Name = "runButton";
|
||||
runButton.Size = new System.Drawing.Size(100, 30);
|
||||
runButton.Size = new System.Drawing.Size(65, 87);
|
||||
runButton.TabIndex = 11;
|
||||
runButton.Text = "Start";
|
||||
runButton.UseVisualStyleBackColor = true;
|
||||
@@ -66,7 +68,7 @@ namespace ExportDXF.Forms
|
||||
// label3
|
||||
//
|
||||
label3.AutoSize = true;
|
||||
label3.Location = new System.Drawing.Point(26, 46);
|
||||
label3.Location = new System.Drawing.Point(26, 77);
|
||||
label3.Name = "label3";
|
||||
label3.Size = new System.Drawing.Size(105, 17);
|
||||
label3.TabIndex = 2;
|
||||
@@ -76,7 +78,7 @@ namespace ExportDXF.Forms
|
||||
//
|
||||
viewFlipDeciderBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
viewFlipDeciderBox.FormattingEnabled = true;
|
||||
viewFlipDeciderBox.Location = new System.Drawing.Point(137, 43);
|
||||
viewFlipDeciderBox.Location = new System.Drawing.Point(137, 74);
|
||||
viewFlipDeciderBox.Name = "viewFlipDeciderBox";
|
||||
viewFlipDeciderBox.Size = new System.Drawing.Size(365, 25);
|
||||
viewFlipDeciderBox.TabIndex = 3;
|
||||
@@ -87,11 +89,11 @@ namespace ExportDXF.Forms
|
||||
mainTabControl.Controls.Add(logEventsTab);
|
||||
mainTabControl.Controls.Add(bomTab);
|
||||
mainTabControl.Controls.Add(cutTemplatesTab);
|
||||
mainTabControl.Location = new System.Drawing.Point(15, 74);
|
||||
mainTabControl.Location = new System.Drawing.Point(15, 105);
|
||||
mainTabControl.Name = "mainTabControl";
|
||||
mainTabControl.Padding = new System.Drawing.Point(20, 5);
|
||||
mainTabControl.SelectedIndex = 0;
|
||||
mainTabControl.Size = new System.Drawing.Size(599, 330);
|
||||
mainTabControl.Size = new System.Drawing.Size(910, 492);
|
||||
mainTabControl.TabIndex = 12;
|
||||
//
|
||||
// logEventsTab
|
||||
@@ -100,7 +102,7 @@ namespace ExportDXF.Forms
|
||||
logEventsTab.Location = new System.Drawing.Point(4, 30);
|
||||
logEventsTab.Name = "logEventsTab";
|
||||
logEventsTab.Padding = new System.Windows.Forms.Padding(3);
|
||||
logEventsTab.Size = new System.Drawing.Size(591, 296);
|
||||
logEventsTab.Size = new System.Drawing.Size(902, 458);
|
||||
logEventsTab.TabIndex = 0;
|
||||
logEventsTab.Text = "Log Events";
|
||||
logEventsTab.UseVisualStyleBackColor = true;
|
||||
@@ -112,7 +114,7 @@ namespace ExportDXF.Forms
|
||||
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
logEventsDataGrid.Name = "logEventsDataGrid";
|
||||
logEventsDataGrid.Size = new System.Drawing.Size(579, 282);
|
||||
logEventsDataGrid.Size = new System.Drawing.Size(890, 444);
|
||||
logEventsDataGrid.TabIndex = 0;
|
||||
//
|
||||
// bomTab
|
||||
@@ -121,7 +123,7 @@ namespace ExportDXF.Forms
|
||||
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(982, 549);
|
||||
bomTab.Size = new System.Drawing.Size(902, 458);
|
||||
bomTab.TabIndex = 1;
|
||||
bomTab.Text = "Bill Of Materials";
|
||||
bomTab.UseVisualStyleBackColor = true;
|
||||
@@ -133,30 +135,30 @@ namespace ExportDXF.Forms
|
||||
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
bomDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
bomDataGrid.Name = "bomDataGrid";
|
||||
bomDataGrid.Size = new System.Drawing.Size(970, 535);
|
||||
bomDataGrid.Size = new System.Drawing.Size(890, 444);
|
||||
bomDataGrid.TabIndex = 1;
|
||||
//
|
||||
//
|
||||
// cutTemplatesTab
|
||||
//
|
||||
//
|
||||
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
|
||||
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(982, 549);
|
||||
cutTemplatesTab.Size = new System.Drawing.Size(902, 458);
|
||||
cutTemplatesTab.TabIndex = 2;
|
||||
cutTemplatesTab.Text = "Cut Templates";
|
||||
cutTemplatesTab.UseVisualStyleBackColor = true;
|
||||
//
|
||||
//
|
||||
// cutTemplatesDataGrid
|
||||
//
|
||||
//
|
||||
cutTemplatesDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
cutTemplatesDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
|
||||
cutTemplatesDataGrid.Size = new System.Drawing.Size(970, 535);
|
||||
cutTemplatesDataGrid.Size = new System.Drawing.Size(890, 447);
|
||||
cutTemplatesDataGrid.TabIndex = 2;
|
||||
//
|
||||
//
|
||||
// equipmentBox
|
||||
//
|
||||
equipmentBox.FormattingEnabled = true;
|
||||
@@ -191,10 +193,28 @@ namespace ExportDXF.Forms
|
||||
drawingNoBox.Size = new System.Drawing.Size(119, 25);
|
||||
drawingNoBox.TabIndex = 13;
|
||||
//
|
||||
// titleLabel
|
||||
//
|
||||
titleLabel.AutoSize = true;
|
||||
titleLabel.Location = new System.Drawing.Point(99, 46);
|
||||
titleLabel.Name = "titleLabel";
|
||||
titleLabel.Size = new System.Drawing.Size(32, 17);
|
||||
titleLabel.TabIndex = 14;
|
||||
titleLabel.Text = "Title";
|
||||
//
|
||||
// titleBox
|
||||
//
|
||||
titleBox.Location = new System.Drawing.Point(137, 43);
|
||||
titleBox.Name = "titleBox";
|
||||
titleBox.Size = new System.Drawing.Size(365, 25);
|
||||
titleBox.TabIndex = 15;
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
ClientSize = new System.Drawing.Size(626, 416);
|
||||
ClientSize = new System.Drawing.Size(937, 609);
|
||||
Controls.Add(titleBox);
|
||||
Controls.Add(titleLabel);
|
||||
Controls.Add(drawingNoBox);
|
||||
Controls.Add(equipmentBox);
|
||||
Controls.Add(mainTabControl);
|
||||
@@ -237,5 +257,7 @@ namespace ExportDXF.Forms
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.ComboBox drawingNoBox;
|
||||
private System.Windows.Forms.Label titleLabel;
|
||||
private System.Windows.Forms.TextBox titleBox;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using ExportDXF.Data;
|
||||
using ExportDXF.ApiClient;
|
||||
using ExportDXF.Extensions;
|
||||
using ExportDXF.Models;
|
||||
using ExportDXF.Services;
|
||||
@@ -18,15 +18,14 @@ namespace ExportDXF.Forms
|
||||
{
|
||||
private readonly ISolidWorksService _solidWorksService;
|
||||
private readonly IDxfExportService _exportService;
|
||||
private readonly IFileExportService _fileExportService;
|
||||
private readonly Func<ExportDxfDbContext> _dbContextFactory;
|
||||
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, IFileExportService fileExportService, Func<ExportDxfDbContext> dbContextFactory = null)
|
||||
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFabWorksApiClient apiClient)
|
||||
{
|
||||
InitializeComponent();
|
||||
_solidWorksService = solidWorksService ??
|
||||
@@ -34,9 +33,8 @@ namespace ExportDXF.Forms
|
||||
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
|
||||
_exportService = exportService ??
|
||||
throw new ArgumentNullException(nameof(exportService));
|
||||
_fileExportService = fileExportService ??
|
||||
throw new ArgumentNullException(nameof(fileExportService));
|
||||
_dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext());
|
||||
_apiClient = apiClient ??
|
||||
throw new ArgumentNullException(nameof(apiClient));
|
||||
_logEvents = new BindingList<LogEvent>();
|
||||
_bomItems = new BindingList<BomItem>();
|
||||
_cutTemplates = new BindingList<CutTemplate>();
|
||||
@@ -70,9 +68,10 @@ namespace ExportDXF.Forms
|
||||
LogMessage("Connecting to SolidWorks, this may take a minute...");
|
||||
await _solidWorksService.ConnectAsync();
|
||||
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
|
||||
LogMessage($"Output folder: {_fileExportService.OutputFolder}");
|
||||
LogMessage("Files will be uploaded to FabWorks API");
|
||||
await LoadDrawingDropdownsAsync();
|
||||
LogMessage("Ready");
|
||||
UpdateActiveDocumentDisplay();
|
||||
await UpdateActiveDocumentDisplayAsync();
|
||||
runButton.Enabled = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -274,74 +273,60 @@ namespace ExportDXF.Forms
|
||||
}
|
||||
|
||||
private void InitializeDrawingDropdowns()
|
||||
{
|
||||
// Wire up event handler; actual data loading happens in LoadDrawingDropdownsAsync
|
||||
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
|
||||
}
|
||||
|
||||
private async Task LoadDrawingDropdownsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var db = _dbContextFactory())
|
||||
var equipmentNumbers = await _apiClient.GetEquipmentNumbersAsync();
|
||||
|
||||
equipmentBox.Items.Clear();
|
||||
equipmentBox.Items.Add("");
|
||||
foreach (var eq in equipmentNumbers)
|
||||
{
|
||||
// Get all drawing numbers from the database
|
||||
var drawingNumbers = db.ExportRecords
|
||||
.Select(r => r.DrawingNumber)
|
||||
.Where(d => !string.IsNullOrEmpty(d))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
// Parse into DrawingInfo objects
|
||||
_allDrawings = drawingNumbers
|
||||
.Select(DrawingInfo.Parse)
|
||||
.Where(d => d != null)
|
||||
.Distinct()
|
||||
.OrderBy(d => d.EquipmentNo)
|
||||
.ThenBy(d => d.DrawingNo)
|
||||
.ToList();
|
||||
|
||||
// Get distinct equipment numbers
|
||||
var equipmentNumbers = _allDrawings
|
||||
.Select(d => d.EquipmentNo)
|
||||
.Distinct()
|
||||
.OrderBy(e => e)
|
||||
.ToList();
|
||||
|
||||
// Populate equipment dropdown
|
||||
equipmentBox.Items.Clear();
|
||||
equipmentBox.Items.Add(""); // Empty option for "all"
|
||||
foreach (var eq in equipmentNumbers)
|
||||
{
|
||||
equipmentBox.Items.Add(eq);
|
||||
}
|
||||
|
||||
// Populate drawing dropdown with all drawings initially
|
||||
UpdateDrawingDropdown();
|
||||
|
||||
// Wire up event handler for equipment selection change
|
||||
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
|
||||
equipmentBox.Items.Add(eq);
|
||||
}
|
||||
|
||||
// Clear _allDrawings — drawing list is now loaded on equipment selection
|
||||
_allDrawings = new List<DrawingInfo>();
|
||||
await UpdateDrawingDropdownAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Database might not exist yet - that's OK
|
||||
System.Diagnostics.Debug.WriteLine($"Failed to load drawings from database: {ex.Message}");
|
||||
// API might not be available yet - that's OK
|
||||
System.Diagnostics.Debug.WriteLine($"Failed to load equipment numbers from API: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
private async void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateDrawingDropdown();
|
||||
await UpdateDrawingDropdownAsync();
|
||||
}
|
||||
|
||||
private void UpdateDrawingDropdown()
|
||||
private async Task UpdateDrawingDropdownAsync()
|
||||
{
|
||||
var selectedEquipment = equipmentBox.SelectedItem?.ToString();
|
||||
|
||||
var filteredDrawings = string.IsNullOrEmpty(selectedEquipment)
|
||||
? _allDrawings
|
||||
: _allDrawings.Where(d => d.EquipmentNo == selectedEquipment).ToList();
|
||||
|
||||
drawingNoBox.Items.Clear();
|
||||
drawingNoBox.Items.Add(""); // Empty option
|
||||
foreach (var drawing in filteredDrawings)
|
||||
drawingNoBox.Items.Add("");
|
||||
|
||||
try
|
||||
{
|
||||
drawingNoBox.Items.Add(drawing.DrawingNo);
|
||||
var drawingNumbers = await _apiClient.GetDrawingNumbersByEquipmentAsync(
|
||||
string.IsNullOrEmpty(selectedEquipment) ? null : selectedEquipment);
|
||||
|
||||
foreach (var dn in drawingNumbers)
|
||||
{
|
||||
drawingNoBox.Items.Add(dn);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// API might not be available
|
||||
}
|
||||
|
||||
if (drawingNoBox.Items.Count > 0)
|
||||
@@ -380,16 +365,21 @@ namespace ExportDXF.Forms
|
||||
// Use equipment/drawing values from the UI dropdowns
|
||||
var equipment = equipmentBox.Text?.Trim();
|
||||
var drawingNo = drawingNoBox.Text?.Trim();
|
||||
var filePrefix = !string.IsNullOrEmpty(equipment) && !string.IsNullOrEmpty(drawingNo)
|
||||
? $"{equipment} {drawingNo}"
|
||||
var filePrefix = !string.IsNullOrEmpty(equipment)
|
||||
? (!string.IsNullOrEmpty(drawingNo) ? $"{equipment} {drawingNo}" : equipment)
|
||||
: activeDoc.Title;
|
||||
|
||||
var viewFlipDecider = GetSelectedViewFlipDecider();
|
||||
var title = titleBox.Text?.Trim();
|
||||
|
||||
var exportContext = new ExportContext
|
||||
{
|
||||
ActiveDocument = activeDoc,
|
||||
ViewFlipDecider = viewFlipDecider,
|
||||
FilePrefix = filePrefix,
|
||||
Equipment = equipment,
|
||||
DrawingNo = drawingNo,
|
||||
Title = string.IsNullOrEmpty(title) ? null : title,
|
||||
EquipmentId = null,
|
||||
CancellationToken = token,
|
||||
ProgressCallback = (msg, level, file) => LogMessage(msg, level, file),
|
||||
@@ -401,11 +391,11 @@ namespace ExportDXF.Forms
|
||||
_cutTemplates.Clear();
|
||||
|
||||
LogMessage($"Started at {DateTime.Now:t}");
|
||||
LogMessage($"Exporting to: {_fileExportService.OutputFolder}");
|
||||
LogMessage("Exporting (files will be uploaded to API)...");
|
||||
|
||||
_solidWorksService.SetCommandInProgress(true);
|
||||
|
||||
await Task.Run(() => _exportService.Export(exportContext), token);
|
||||
await Task.Run(async () => await _exportService.ExportAsync(exportContext), token);
|
||||
|
||||
LogMessage("Done.");
|
||||
}
|
||||
@@ -452,32 +442,61 @@ namespace ExportDXF.Forms
|
||||
runButton.Enabled = true;
|
||||
}
|
||||
|
||||
private void OnActiveDocumentChanged(object sender, EventArgs e)
|
||||
private async void OnActiveDocumentChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action(() => OnActiveDocumentChanged(sender, e)));
|
||||
Invoke(new Action(async () => await UpdateActiveDocumentDisplayAsync()));
|
||||
return;
|
||||
}
|
||||
UpdateActiveDocumentDisplay();
|
||||
await UpdateActiveDocumentDisplayAsync();
|
||||
}
|
||||
|
||||
private void UpdateActiveDocumentDisplay()
|
||||
private async Task UpdateActiveDocumentDisplayAsync()
|
||||
{
|
||||
var activeDoc = _solidWorksService.GetActiveDocument();
|
||||
var docTitle = activeDoc?.Title ?? "No Document Open";
|
||||
this.Text = $"ExportDXF - {docTitle}";
|
||||
|
||||
// Parse the file name and fill Equipment/Drawing dropdowns
|
||||
if (activeDoc != null)
|
||||
if (activeDoc == null)
|
||||
return;
|
||||
|
||||
// Try API first: look up the most recent export for this file path
|
||||
DrawingInfo drawingInfo = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(activeDoc.FilePath))
|
||||
{
|
||||
var drawingInfo = DrawingInfo.Parse(activeDoc.Title);
|
||||
if (drawingInfo != null)
|
||||
drawingInfo = await LookupDrawingInfoFromHistoryAsync(activeDoc.FilePath);
|
||||
}
|
||||
|
||||
// Fall back to parsing the document title
|
||||
if (drawingInfo == null)
|
||||
{
|
||||
drawingInfo = DrawingInfo.Parse(activeDoc.Title);
|
||||
}
|
||||
|
||||
if (drawingInfo != null)
|
||||
{
|
||||
// Detach event to prevent async race when setting equipment
|
||||
equipmentBox.SelectedIndexChanged -= EquipmentBox_SelectedIndexChanged;
|
||||
|
||||
if (!string.IsNullOrEmpty(drawingInfo.EquipmentNo))
|
||||
{
|
||||
if (!equipmentBox.Items.Contains(drawingInfo.EquipmentNo))
|
||||
equipmentBox.Items.Add(drawingInfo.EquipmentNo);
|
||||
equipmentBox.Text = drawingInfo.EquipmentNo;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -485,6 +504,26 @@ namespace ExportDXF.Forms
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ExportDXF.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ExtractCutTemplate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ContentHash",
|
||||
table: "BomItems");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CutTemplateName",
|
||||
table: "BomItems");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultBendRadius",
|
||||
table: "BomItems");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DxfFilePath",
|
||||
table: "BomItems");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "KFactor",
|
||||
table: "BomItems");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Thickness",
|
||||
table: "BomItems");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CutTemplates",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DxfFilePath = table.Column<string>(type: "nvarchar(max)", 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.CreateIndex(
|
||||
name: "IX_CutTemplates_BomItemId",
|
||||
table: "CutTemplates",
|
||||
column: "BomItemId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CutTemplates");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ContentHash",
|
||||
table: "BomItems",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CutTemplateName",
|
||||
table: "BomItems",
|
||||
type: "nvarchar(100)",
|
||||
maxLength: 100,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<double>(
|
||||
name: "DefaultBendRadius",
|
||||
table: "BomItems",
|
||||
type: "float",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DxfFilePath",
|
||||
table: "BomItems",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<double>(
|
||||
name: "KFactor",
|
||||
table: "BomItems",
|
||||
type: "float",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<double>(
|
||||
name: "Thickness",
|
||||
table: "BomItems",
|
||||
type: "float",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,21 @@ namespace ExportDXF.Services
|
||||
/// </summary>
|
||||
public string FilePrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Equipment number from the UI (e.g., "5028").
|
||||
/// </summary>
|
||||
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>
|
||||
|
||||
@@ -7,10 +7,13 @@ namespace ExportDXF.Models
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string DrawingNumber { get; set; }
|
||||
public string EquipmentNo { get; set; }
|
||||
public string DrawingNo { get; set; }
|
||||
public string SourceFilePath { get; set; }
|
||||
public string OutputFolder { get; set; }
|
||||
public DateTime ExportedAt { get; set; }
|
||||
public string ExportedBy { get; set; }
|
||||
public string PdfContentHash { get; set; }
|
||||
|
||||
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
|
||||
}
|
||||
|
||||
@@ -61,5 +61,16 @@ namespace ExportDXF.Services
|
||||
/// The SolidWorks component reference.
|
||||
/// </summary>
|
||||
public Component2 Component { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 content hash of the exported DXF (transient, not persisted).
|
||||
/// </summary>
|
||||
public string ContentHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full path to the locally-exported DXF temp file (transient, not persisted).
|
||||
/// Set after successful export; used for upload to the API.
|
||||
/// </summary>
|
||||
public string LocalTempPath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
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
|
||||
@@ -28,11 +33,11 @@ namespace ExportDXF
|
||||
/// </summary>
|
||||
public class ServiceContainer
|
||||
{
|
||||
private readonly string _outputFolder;
|
||||
private readonly string _apiBaseUrl;
|
||||
|
||||
public ServiceContainer()
|
||||
{
|
||||
_outputFolder = ConfigurationManager.AppSettings["ExportOutputFolder"] ?? @"C:\ExportDXF\Output";
|
||||
_apiBaseUrl = ConfigurationManager.AppSettings["FabWorksApiUrl"] ?? "http://localhost:5206";
|
||||
}
|
||||
|
||||
public MainForm ResolveMainForm()
|
||||
@@ -41,16 +46,71 @@ namespace ExportDXF
|
||||
var bomExtractor = new BomExtractor();
|
||||
var partExporter = new PartExporter();
|
||||
var drawingExporter = new DrawingExporter();
|
||||
var fileExportService = new FileExportService(_outputFolder);
|
||||
|
||||
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,
|
||||
fileExportService);
|
||||
apiClient);
|
||||
|
||||
return new MainForm(solidWorksService, exportService, fileExportService);
|
||||
return new MainForm(solidWorksService, exportService, apiClient);
|
||||
}
|
||||
|
||||
private void EnsureApiRunning()
|
||||
{
|
||||
// Check if API is already responding
|
||||
using (var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(2) })
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = probe.GetAsync(_apiBaseUrl + "/api/exports?take=1").Result;
|
||||
if (response.IsSuccessStatusCode)
|
||||
return; // already running
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Find the API executable relative to this assembly
|
||||
var exeDir = AppContext.BaseDirectory;
|
||||
var apiExe = Path.GetFullPath(Path.Combine(exeDir, @"..\..\..\FabWorks.Api\bin\Debug\net8.0\FabWorks.Api.exe"));
|
||||
if (!File.Exists(apiExe))
|
||||
return; // can't find it, skip
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = apiExe,
|
||||
WorkingDirectory = Path.GetDirectoryName(apiExe),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
Process.Start(startInfo);
|
||||
|
||||
// Wait up to 10 seconds for API to become ready
|
||||
using (var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(2) })
|
||||
{
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
try
|
||||
{
|
||||
var response = probe.GetAsync(_apiBaseUrl + "/api/exports?take=1").Result;
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
using ExportDXF.Data;
|
||||
using ExportDXF.ApiClient;
|
||||
using ExportDXF.Extensions;
|
||||
using ExportDXF.ItemExtractors;
|
||||
using ExportDXF.Models;
|
||||
using ExportDXF.Utilities;
|
||||
using ExportDXF;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
@@ -19,11 +19,12 @@ namespace ExportDXF.Services
|
||||
/// Exports the document specified in the context to DXF format.
|
||||
/// </summary>
|
||||
/// <param name="context">The export context containing all necessary information.</param>
|
||||
void Export(ExportContext context);
|
||||
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
|
||||
{
|
||||
@@ -31,29 +32,26 @@ namespace ExportDXF.Services
|
||||
private readonly IBomExtractor _bomExtractor;
|
||||
private readonly IPartExporter _partExporter;
|
||||
private readonly IDrawingExporter _drawingExporter;
|
||||
private readonly IFileExportService _fileExportService;
|
||||
private readonly Func<ExportDxfDbContext> _dbContextFactory;
|
||||
private readonly IFabWorksApiClient _apiClient;
|
||||
|
||||
public DxfExportService(
|
||||
ISolidWorksService solidWorksService,
|
||||
IBomExtractor bomExtractor,
|
||||
IPartExporter partExporter,
|
||||
IDrawingExporter drawingExporter,
|
||||
IFileExportService fileExportService,
|
||||
Func<ExportDxfDbContext> dbContextFactory = null)
|
||||
IFabWorksApiClient apiClient)
|
||||
{
|
||||
_solidWorksService = solidWorksService ?? throw new ArgumentNullException(nameof(solidWorksService));
|
||||
_bomExtractor = bomExtractor ?? throw new ArgumentNullException(nameof(bomExtractor));
|
||||
_partExporter = partExporter ?? throw new ArgumentNullException(nameof(partExporter));
|
||||
_drawingExporter = drawingExporter ?? throw new ArgumentNullException(nameof(drawingExporter));
|
||||
_fileExportService = fileExportService ?? throw new ArgumentNullException(nameof(fileExportService));
|
||||
_dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext());
|
||||
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports the document specified in the context to DXF format.
|
||||
/// </summary>
|
||||
public void Export(ExportContext context)
|
||||
public async Task ExportAsync(ExportContext context)
|
||||
{
|
||||
if (context == null)
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
@@ -62,23 +60,26 @@ namespace ExportDXF.Services
|
||||
SetupExportContext(context);
|
||||
|
||||
var startTime = DateTime.Now;
|
||||
var tempDir = CreateTempWorkDir();
|
||||
|
||||
try
|
||||
{
|
||||
_solidWorksService.EnableUserControl(false);
|
||||
|
||||
var drawingNumber = ParseDrawingNumber(context);
|
||||
|
||||
switch (context.ActiveDocument.DocumentType)
|
||||
{
|
||||
case DocumentType.Part:
|
||||
ExportPart(context);
|
||||
await ExportPartAsync(context, tempDir, drawingNumber);
|
||||
break;
|
||||
|
||||
case DocumentType.Assembly:
|
||||
ExportAssembly(context);
|
||||
await ExportAssemblyAsync(context, tempDir, drawingNumber);
|
||||
break;
|
||||
|
||||
case DocumentType.Drawing:
|
||||
ExportDrawing(context);
|
||||
await ExportDrawingAsync(context, drawingNumber, tempDir);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -90,6 +91,7 @@ namespace ExportDXF.Services
|
||||
{
|
||||
CleanupExportContext(context);
|
||||
_solidWorksService.EnableUserControl(true);
|
||||
CleanupTempDir(tempDir);
|
||||
|
||||
var duration = DateTime.Now - startTime;
|
||||
LogProgress(context, $"Run time: {duration.ToReadableFormat()}");
|
||||
@@ -98,7 +100,7 @@ namespace ExportDXF.Services
|
||||
|
||||
#region Export Methods by Document Type
|
||||
|
||||
private void ExportPart(ExportContext context)
|
||||
private async Task ExportPartAsync(ExportContext context, string tempDir, string drawingNumber)
|
||||
{
|
||||
LogProgress(context, "Active document is a Part");
|
||||
|
||||
@@ -109,11 +111,54 @@ namespace ExportDXF.Services
|
||||
return;
|
||||
}
|
||||
|
||||
// Export directly to the output folder
|
||||
_partExporter.ExportSinglePart(part, _fileExportService.OutputFolder, context);
|
||||
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
|
||||
var item = _partExporter.ExportSinglePart(part, tempDir, context);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
// Check if this part+config already has a BOM item for this drawing
|
||||
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
|
||||
item.ItemNo = existingItemNo ?? await GetNextItemNumberAsync(drawingNumber);
|
||||
|
||||
var bomItem = new BomItem
|
||||
{
|
||||
ExportRecordId = exportRecord?.Id ?? 0,
|
||||
ItemNo = item.ItemNo,
|
||||
PartNo = item.FileName ?? item.PartName ?? "",
|
||||
SortOrder = 0,
|
||||
Qty = item.Quantity,
|
||||
TotalQty = item.Quantity,
|
||||
Description = item.Description ?? "",
|
||||
PartName = item.PartName ?? "",
|
||||
ConfigurationName = item.Configuration ?? "",
|
||||
Material = item.Material ?? ""
|
||||
};
|
||||
|
||||
// Upload DXF to API and get stored path
|
||||
if (!string.IsNullOrEmpty(item.LocalTempPath))
|
||||
{
|
||||
var uploadResult = await UploadDxfAsync(item, context);
|
||||
if (uploadResult != null)
|
||||
{
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
{
|
||||
DxfFilePath = uploadResult.StoredFilePath,
|
||||
ContentHash = item.ContentHash,
|
||||
Thickness = item.Thickness > 0 ? item.Thickness : null,
|
||||
KFactor = item.KFactor > 0 ? item.KFactor : null,
|
||||
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
|
||||
if (exportRecord != null)
|
||||
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExportAssembly(ExportContext context)
|
||||
private async Task ExportAssemblyAsync(ExportContext context, string tempDir, string drawingNumber)
|
||||
{
|
||||
LogProgress(context, "Active document is an Assembly");
|
||||
LogProgress(context, "Fetching components...");
|
||||
@@ -135,14 +180,33 @@ namespace ExportDXF.Services
|
||||
|
||||
LogProgress(context, $"Found {items.Count} item(s).");
|
||||
|
||||
// Export directly to the output folder
|
||||
ExportItems(items, _fileExportService.OutputFolder, context);
|
||||
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
|
||||
|
||||
// Check existing BOM items and reuse item numbers, or assign new ones
|
||||
var nextNum = int.Parse(await GetNextItemNumberAsync(drawingNumber));
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.ItemNo))
|
||||
{
|
||||
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
|
||||
if (existingItemNo != null)
|
||||
{
|
||||
item.ItemNo = existingItemNo;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.ItemNo = nextNum.ToString();
|
||||
nextNum++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
|
||||
}
|
||||
|
||||
private void ExportDrawing(ExportContext context)
|
||||
private async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir)
|
||||
{
|
||||
LogProgress(context, "Active document is a Drawing");
|
||||
LogProgress(context, "Finding BOM tables...");
|
||||
|
||||
var drawing = context.ActiveDocument.NativeDocument as DrawingDoc;
|
||||
if (drawing == null)
|
||||
@@ -151,103 +215,115 @@ namespace ExportDXF.Services
|
||||
return;
|
||||
}
|
||||
|
||||
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
|
||||
|
||||
if (items == null || items.Count == 0)
|
||||
{
|
||||
LogProgress(context, "Error: Bill of materials not found.", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
LogProgress(context, $"Found {items.Count} component(s)");
|
||||
|
||||
// Determine drawing number for file naming
|
||||
var drawingNumber = ParseDrawingNumber(context);
|
||||
|
||||
// Resolve output folder: /{outputDir}/{equipmentNo}/{drawingNo}/ or flat fallback
|
||||
var drawingOutputFolder = _fileExportService.GetDrawingOutputFolder(drawingNumber);
|
||||
|
||||
// Export drawing to PDF
|
||||
var tempDir = CreateTempWorkDir();
|
||||
// Export drawing to PDF in temp dir
|
||||
_drawingExporter.ExportToPdf(drawing, tempDir, context);
|
||||
|
||||
// Copy PDF to output folder with versioning
|
||||
string pdfStashPath = null;
|
||||
string savedPdfPath = null;
|
||||
// Create export record via API
|
||||
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
|
||||
|
||||
// Upload PDF to API with versioning
|
||||
try
|
||||
{
|
||||
var pdfs = Directory.GetFiles(tempDir, "*.pdf");
|
||||
if (pdfs.Length > 0)
|
||||
{
|
||||
// Determine the destination path to stash the existing file
|
||||
var pdfFileName = !string.IsNullOrEmpty(drawingNumber)
|
||||
? $"{drawingNumber}.pdf"
|
||||
: Path.GetFileName(pdfs[0]);
|
||||
var pdfDestPath = Path.Combine(drawingOutputFolder, pdfFileName);
|
||||
var pdfTempPath = pdfs[0];
|
||||
var pdfHash = ContentHasher.ComputePdfContentHash(pdfTempPath);
|
||||
|
||||
pdfStashPath = _fileExportService.StashFile(pdfDestPath);
|
||||
savedPdfPath = _fileExportService.SavePdfFile(pdfs[0], drawingNumber, drawingOutputFolder);
|
||||
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 save error: {ex.Message}", LogLevel.Error);
|
||||
LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// Create export record in database
|
||||
ExportRecord exportRecord = null;
|
||||
try
|
||||
// Extract BOM items from drawing tables
|
||||
LogProgress(context, "Finding BOM tables...");
|
||||
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
|
||||
|
||||
if (items != null && items.Count > 0)
|
||||
{
|
||||
using (var db = _dbContextFactory())
|
||||
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)
|
||||
{
|
||||
db.Database.Migrate();
|
||||
exportRecord = new ExportRecord
|
||||
LogProgress(context, "No referenced part found in drawing views.", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
LogProgress(context, $"Found referenced part, exporting as single part...");
|
||||
|
||||
var item = _partExporter.ExportSinglePart(part, tempDir, context);
|
||||
if (item != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configuration))
|
||||
item.Configuration = configuration;
|
||||
|
||||
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
|
||||
item.ItemNo = existingItemNo ?? await GetNextItemNumberAsync(drawingNumber);
|
||||
|
||||
var bomItem = new BomItem
|
||||
{
|
||||
DrawingNumber = drawingNumber ?? context.ActiveDocument.Title,
|
||||
SourceFilePath = context.ActiveDocument.FilePath,
|
||||
OutputFolder = drawingOutputFolder,
|
||||
ExportedAt = DateTime.Now,
|
||||
ExportedBy = System.Environment.UserName
|
||||
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 ?? ""
|
||||
};
|
||||
|
||||
// Handle PDF versioning - compute hash and compare with previous
|
||||
if (savedPdfPath != null)
|
||||
if (!string.IsNullOrEmpty(item.LocalTempPath))
|
||||
{
|
||||
HandlePdfVersioning(savedPdfPath, exportRecord.DrawingNumber, exportRecord, context);
|
||||
|
||||
// Archive or discard old PDF based on hash comparison
|
||||
if (pdfStashPath != null)
|
||||
var uploadResult = await UploadDxfAsync(item, context);
|
||||
if (uploadResult != null)
|
||||
{
|
||||
var previousRecord = db.ExportRecords
|
||||
.Where(r => r.DrawingNumber == exportRecord.DrawingNumber && r.PdfContentHash != null)
|
||||
.OrderByDescending(r => r.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (previousRecord != null && previousRecord.PdfContentHash == exportRecord.PdfContentHash)
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
{
|
||||
_fileExportService.DiscardStash(pdfStashPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_fileExportService.ArchiveFile(pdfStashPath, savedPdfPath);
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
db.ExportRecords.Add(exportRecord);
|
||||
db.SaveChanges();
|
||||
LogProgress(context, $"Created export record (ID: {exportRecord.Id})", LogLevel.Info);
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
|
||||
if (exportRecord != null)
|
||||
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Clean up stash on error
|
||||
_fileExportService.DiscardStash(pdfStashPath);
|
||||
LogProgress(context, $"Database error creating export record: {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// Export parts to DXF and save BOM items
|
||||
ExportItems(items, drawingOutputFolder, context, exportRecord?.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -306,7 +382,7 @@ namespace ExportDXF.Services
|
||||
}
|
||||
}
|
||||
|
||||
private void ExportItems(List<Item> items, string saveDirectory, ExportContext context, int? exportRecordId = null)
|
||||
private async Task ExportItemsAsync(List<Item> items, string tempDir, ExportContext context, int? exportRecordId = null)
|
||||
{
|
||||
int successCount = 0;
|
||||
int skippedCount = 0;
|
||||
@@ -324,7 +400,7 @@ namespace ExportDXF.Services
|
||||
try
|
||||
{
|
||||
// PartExporter will handle template drawing creation through context
|
||||
_partExporter.ExportItem(item, saveDirectory, context);
|
||||
_partExporter.ExportItem(item, tempDir, context);
|
||||
|
||||
// Always create BomItem for every item (sheet metal or not)
|
||||
var bomItem = new BomItem
|
||||
@@ -341,23 +417,23 @@ namespace ExportDXF.Services
|
||||
Material = item.Material ?? ""
|
||||
};
|
||||
|
||||
// Only create CutTemplate if DXF was exported successfully
|
||||
if (!string.IsNullOrEmpty(item.FileName))
|
||||
// Only upload and create CutTemplate if DXF was exported successfully
|
||||
if (!string.IsNullOrEmpty(item.LocalTempPath))
|
||||
{
|
||||
successCount++;
|
||||
|
||||
var dxfPath = Path.Combine(saveDirectory, item.FileName + ".dxf");
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
var uploadResult = await UploadDxfAsync(item, context);
|
||||
if (uploadResult != null)
|
||||
{
|
||||
DxfFilePath = dxfPath,
|
||||
ContentHash = item.ContentHash,
|
||||
Thickness = item.Thickness > 0 ? item.Thickness : null,
|
||||
KFactor = item.KFactor > 0 ? item.KFactor : null,
|
||||
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
|
||||
};
|
||||
|
||||
// Compare hash with previous export to decide archive/discard
|
||||
HandleDxfVersioning(item, dxfPath, context);
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
{
|
||||
DxfFilePath = uploadResult.StoredFilePath,
|
||||
ContentHash = item.ContentHash,
|
||||
Thickness = item.Thickness > 0 ? item.Thickness : null,
|
||||
KFactor = item.KFactor > 0 ? item.KFactor : null,
|
||||
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -367,21 +443,10 @@ namespace ExportDXF.Services
|
||||
// Add to UI
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
|
||||
// Save BOM item to database if we have an export record
|
||||
// Save BOM item via API if we have an export record
|
||||
if (exportRecordId.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var db = _dbContextFactory())
|
||||
{
|
||||
db.BomItems.Add(bomItem);
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
catch (Exception dbEx)
|
||||
{
|
||||
LogProgress(context, $"Database error saving BOM item: {dbEx.Message}", LogLevel.Error);
|
||||
}
|
||||
await SaveBomItemAsync(exportRecordId.Value, bomItem, context);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -398,86 +463,144 @@ namespace ExportDXF.Services
|
||||
|
||||
if (exportRecordId.HasValue)
|
||||
{
|
||||
LogProgress(context, $"BOM items saved to database (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info);
|
||||
LogProgress(context, $"BOM items saved (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Versioning
|
||||
#region File Upload
|
||||
|
||||
private void HandleDxfVersioning(Item item, string dxfPath, ExportContext context)
|
||||
private async Task<ApiFileUploadResponse> UploadDxfAsync(Item item, ExportContext context)
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.ContentHash))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = _dbContextFactory())
|
||||
{
|
||||
var previousCutTemplate = db.CutTemplates
|
||||
.Where(ct => ct.DxfFilePath == dxfPath && ct.ContentHash != null)
|
||||
.OrderByDescending(ct => ct.Id)
|
||||
.FirstOrDefault();
|
||||
var result = await _apiClient.UploadDxfAsync(
|
||||
item.LocalTempPath,
|
||||
context.Equipment,
|
||||
context.DrawingNo,
|
||||
item.ItemNo,
|
||||
item.ContentHash);
|
||||
|
||||
if (previousCutTemplate != null && previousCutTemplate.ContentHash == item.ContentHash)
|
||||
{
|
||||
// Content unchanged - discard the stashed file
|
||||
_fileExportService.DiscardStash(item.StashedFilePath);
|
||||
LogProgress(context, $"DXF unchanged: {item.FileName}.dxf", LogLevel.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Content changed or first export - archive the old file
|
||||
if (!string.IsNullOrEmpty(item.StashedFilePath))
|
||||
{
|
||||
_fileExportService.ArchiveFile(item.StashedFilePath, dxfPath);
|
||||
LogProgress(context, $"DXF updated, previous version archived: {item.FileName}.dxf", LogLevel.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogProgress(context, $"Exported: {item.FileName}.dxf", LogLevel.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.WasUnchanged)
|
||||
LogProgress(context, $"DXF unchanged: {result.FileName}", LogLevel.Info);
|
||||
else if (result.IsNewFile)
|
||||
LogProgress(context, $"Exported: {result.FileName}", LogLevel.Info);
|
||||
else
|
||||
LogProgress(context, $"DXF updated: {result.FileName}", LogLevel.Info);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Don't fail the export if versioning fails - just discard the stash
|
||||
_fileExportService.DiscardStash(item.StashedFilePath);
|
||||
LogProgress(context, $"Versioning check failed for {item.FileName}: {ex.Message}", LogLevel.Warning);
|
||||
LogProgress(context, $"DXF upload failed for {item.FileName}: {ex.Message}", LogLevel.Warning);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePdfVersioning(string pdfPath, string drawingNumber, ExportRecord exportRecord, ExportContext context)
|
||||
#endregion
|
||||
|
||||
#region API Helpers
|
||||
|
||||
private async Task<ExportRecord> CreateExportRecordAsync(ExportContext context, string drawingNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newHash = ContentHasher.ComputeFileHash(pdfPath);
|
||||
var dto = await _apiClient.CreateExportAsync(
|
||||
drawingNumber ?? context.ActiveDocument.Title,
|
||||
context.Equipment ?? "",
|
||||
context.DrawingNo ?? "",
|
||||
context.ActiveDocument.FilePath,
|
||||
"", // Output folder is now managed by the API
|
||||
context.Title);
|
||||
|
||||
using (var db = _dbContextFactory())
|
||||
var record = new ExportRecord
|
||||
{
|
||||
var previousRecord = db.ExportRecords
|
||||
.Where(r => r.DrawingNumber == drawingNumber && r.PdfContentHash != null)
|
||||
.OrderByDescending(r => r.Id)
|
||||
.FirstOrDefault();
|
||||
Id = dto.Id,
|
||||
DrawingNumber = dto.DrawingNumber,
|
||||
EquipmentNo = dto.EquipmentNo,
|
||||
DrawingNo = dto.DrawingNo,
|
||||
SourceFilePath = dto.SourceFilePath,
|
||||
OutputFolder = dto.OutputFolder,
|
||||
ExportedAt = dto.ExportedAt,
|
||||
ExportedBy = dto.ExportedBy
|
||||
};
|
||||
|
||||
if (previousRecord != null && previousRecord.PdfContentHash == newHash)
|
||||
{
|
||||
LogProgress(context, $"PDF unchanged: {Path.GetFileName(pdfPath)}", LogLevel.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogProgress(context, $"Saved PDF: {Path.GetFileName(pdfPath)}", LogLevel.Info);
|
||||
}
|
||||
}
|
||||
|
||||
if (exportRecord != null)
|
||||
exportRecord.PdfContentHash = newHash;
|
||||
LogProgress(context, $"Created export record (ID: {record.Id})", LogLevel.Info);
|
||||
return record;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogProgress(context, $"PDF versioning check failed: {ex.Message}", LogLevel.Warning);
|
||||
LogProgress(context, $"API error creating export record: {ex.Message}", LogLevel.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,9 +615,30 @@ namespace ExportDXF.Services
|
||||
return path;
|
||||
}
|
||||
|
||||
private void CleanupTempDir(string tempDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
private string ParseDrawingNumber(ExportContext context)
|
||||
{
|
||||
// Prefer prefix (e.g., "5007 A02 PT"), fallback to active document title
|
||||
// Use explicit Equipment/DrawingNo from the UI when available
|
||||
if (!string.IsNullOrWhiteSpace(context?.Equipment))
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(context?.DrawingNo)
|
||||
? $"{context.Equipment} {context.DrawingNo}"
|
||||
: context.Equipment;
|
||||
}
|
||||
|
||||
// Fallback: parse from prefix or document title
|
||||
var candidate = context?.FilePrefix;
|
||||
var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate);
|
||||
if (info == null)
|
||||
@@ -502,7 +646,7 @@ namespace ExportDXF.Services
|
||||
var title = context?.ActiveDocument?.Title;
|
||||
info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title);
|
||||
}
|
||||
return info != null ? ($"{info.EquipmentNo} {info.DrawingNo}") : null;
|
||||
return info?.ToString();
|
||||
}
|
||||
|
||||
private void ValidateContext(ExportContext context)
|
||||
@@ -514,6 +658,24 @@ namespace ExportDXF.Services
|
||||
throw new ArgumentException("ProgressCallback cannot be null.", nameof(context));
|
||||
}
|
||||
|
||||
private (PartDoc part, string configuration) GetReferencedPartFromViews(DrawingDoc drawing)
|
||||
{
|
||||
var view = (IView)drawing.GetFirstView();
|
||||
// First view is the sheet itself — skip it
|
||||
view = (IView)view.GetNextView();
|
||||
|
||||
while (view != null)
|
||||
{
|
||||
var doc = view.ReferencedDocument;
|
||||
if (doc is PartDoc part)
|
||||
return (part, view.ReferencedConfiguration);
|
||||
|
||||
view = (IView)view.GetNextView();
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null)
|
||||
{
|
||||
context.ProgressCallback?.Invoke(message, level, file);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
public interface IFileExportService
|
||||
{
|
||||
string OutputFolder { get; }
|
||||
string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo);
|
||||
string SavePdfFile(string sourcePath, string drawingNumber);
|
||||
void EnsureOutputFolderExists();
|
||||
}
|
||||
|
||||
public class FileExportService : IFileExportService
|
||||
{
|
||||
public string OutputFolder { get; }
|
||||
|
||||
public FileExportService(string outputFolder)
|
||||
{
|
||||
OutputFolder = outputFolder ?? throw new ArgumentNullException(nameof(outputFolder));
|
||||
EnsureOutputFolderExists();
|
||||
}
|
||||
|
||||
public void EnsureOutputFolderExists()
|
||||
{
|
||||
if (!Directory.Exists(OutputFolder))
|
||||
{
|
||||
Directory.CreateDirectory(OutputFolder);
|
||||
}
|
||||
}
|
||||
|
||||
public string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourcePath))
|
||||
throw new ArgumentNullException(nameof(sourcePath));
|
||||
|
||||
var fileName = !string.IsNullOrEmpty(drawingNumber) && !string.IsNullOrEmpty(itemNo)
|
||||
? $"{drawingNumber} PT{itemNo}.dxf"
|
||||
: Path.GetFileName(sourcePath);
|
||||
|
||||
var destPath = Path.Combine(OutputFolder, fileName);
|
||||
|
||||
// If source and dest are the same, skip copy
|
||||
if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
File.Copy(sourcePath, destPath, overwrite: true);
|
||||
}
|
||||
|
||||
return destPath;
|
||||
}
|
||||
|
||||
public string SavePdfFile(string sourcePath, string drawingNumber)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourcePath))
|
||||
throw new ArgumentNullException(nameof(sourcePath));
|
||||
|
||||
var fileName = !string.IsNullOrEmpty(drawingNumber)
|
||||
? $"{drawingNumber}.pdf"
|
||||
: Path.GetFileName(sourcePath);
|
||||
|
||||
var destPath = Path.Combine(OutputFolder, fileName);
|
||||
|
||||
// If source and dest are the same, skip copy
|
||||
if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
File.Copy(sourcePath, destPath, overwrite: true);
|
||||
}
|
||||
|
||||
return destPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using ExportDXF.Extensions;
|
||||
using ExportDXF.Extensions;
|
||||
using ExportDXF.Models;
|
||||
using ExportDXF.Utilities;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
@@ -15,24 +15,29 @@ namespace ExportDXF.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Exports a single part document to DXF.
|
||||
/// Returns an Item with export metadata (filename, hash, sheet metal properties), or null if export failed.
|
||||
/// </summary>
|
||||
/// <param name="part">The part document to export.</param>
|
||||
/// <param name="saveDirectory">The directory where the DXF file will be saved.</param>
|
||||
/// <param name="saveDirectory">The temp directory where the DXF file will be saved.</param>
|
||||
/// <param name="context">The export context.</param>
|
||||
void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
|
||||
Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Exports an item (component from BOM or assembly) to DXF.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to export.</param>
|
||||
/// <param name="saveDirectory">The directory where the DXF file will be saved.</param>
|
||||
/// <param name="saveDirectory">The temp directory where the DXF file will be saved.</param>
|
||||
/// <param name="context">The export context.</param>
|
||||
void ExportItem(Item item, string saveDirectory, ExportContext context);
|
||||
}
|
||||
|
||||
public class PartExporter : IPartExporter
|
||||
{
|
||||
public void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
|
||||
public PartExporter()
|
||||
{
|
||||
}
|
||||
|
||||
public Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
|
||||
{
|
||||
if (part == null)
|
||||
throw new ArgumentNullException(nameof(part));
|
||||
@@ -49,12 +54,52 @@ namespace ExportDXF.Services
|
||||
|
||||
try
|
||||
{
|
||||
var fileName = GetSinglePartFileName(model, context.FilePrefix);
|
||||
var fileName = GetSinglePartFileName(model, context.Equipment);
|
||||
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
|
||||
|
||||
// Build result item with metadata
|
||||
var item = new Item
|
||||
{
|
||||
PartName = model.GetTitle()?.Replace(".SLDPRT", "") ?? "",
|
||||
Configuration = originalConfigName ?? "",
|
||||
Quantity = 1
|
||||
};
|
||||
|
||||
// Enrich with sheet metal properties and description
|
||||
var sheetMetalProps = SolidWorksHelper.GetSheetMetalProperties(model);
|
||||
if (sheetMetalProps != null)
|
||||
{
|
||||
item.Thickness = sheetMetalProps.Thickness;
|
||||
item.KFactor = sheetMetalProps.KFactor;
|
||||
item.BendRadius = sheetMetalProps.BendRadius;
|
||||
}
|
||||
|
||||
// Get description from custom properties
|
||||
var configPropMgr = model.Extension.CustomPropertyManager[originalConfigName];
|
||||
item.Description = configPropMgr?.Get("Description");
|
||||
if (string.IsNullOrEmpty(item.Description))
|
||||
{
|
||||
var docPropMgr = model.Extension.CustomPropertyManager[""];
|
||||
item.Description = docPropMgr?.Get("Description");
|
||||
}
|
||||
item.Description = TextHelper.RemoveXmlTags(item.Description);
|
||||
|
||||
// Get material
|
||||
item.Material = part.GetMaterialPropertyName2(originalConfigName, out _);
|
||||
|
||||
context.GetOrCreateTemplateDrawing();
|
||||
|
||||
ExportPartToDxf(part, originalConfigName, savePath, context);
|
||||
if (ExportPartToDxf(part, originalConfigName, savePath, context))
|
||||
{
|
||||
item.FileName = Path.GetFileNameWithoutExtension(savePath);
|
||||
item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
|
||||
item.LocalTempPath = savePath;
|
||||
return item;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -94,7 +139,7 @@ namespace ExportDXF.Services
|
||||
|
||||
EnrichItemWithMetadata(item, model, part);
|
||||
|
||||
var fileName = GetItemFileName(item, context.FilePrefix);
|
||||
var fileName = GetItemFileName(item, context);
|
||||
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
|
||||
|
||||
var templateDrawing = context.GetOrCreateTemplateDrawing();
|
||||
@@ -102,6 +147,8 @@ namespace ExportDXF.Services
|
||||
if (ExportPartToDxf(part, item.Component.ReferencedConfiguration, savePath, context))
|
||||
{
|
||||
item.FileName = Path.GetFileNameWithoutExtension(savePath);
|
||||
item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
|
||||
item.LocalTempPath = savePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -187,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);
|
||||
|
||||
@@ -254,40 +301,64 @@ namespace ExportDXF.Services
|
||||
drawing.DeleteSelection(false);
|
||||
}
|
||||
|
||||
private void AddEtchLines(string dxfPath)
|
||||
private void AddEtchLines(string dxfPath, ExportContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var etcher = new EtchBendLines.Etcher();
|
||||
etcher.AddEtchLines(dxfPath);
|
||||
FixDegreeSymbol(dxfPath);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silently fail if etch lines can't be added
|
||||
context.ProgressCallback?.Invoke($"Etch lines failed: {ex.Message}", LogLevel.Warning, Path.GetFileName(dxfPath));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSinglePartFileName(ModelDoc2 model, string prefix)
|
||||
/// <summary>
|
||||
/// Workaround for ACadSharp encoding bug (no upstream fix as of v3.4.9).
|
||||
/// ACadSharp's DxfReader uses $DWGCODEPAGE (ANSI_1252) to decode text, but
|
||||
/// AC1018+ DXF files use UTF-8. The degree symbol ° (UTF-8: C2 B0) gets
|
||||
/// misread as two ANSI_1252 characters: Â (C2) and ° (B0).
|
||||
/// See: https://github.com/DomCR/ACadSharp/issues?q=encoding
|
||||
/// </summary>
|
||||
private static void FixDegreeSymbol(string path)
|
||||
{
|
||||
var text = System.IO.File.ReadAllText(path);
|
||||
if (text.Contains("\u00C2\u00B0"))
|
||||
{
|
||||
text = text.Replace("\u00C2\u00B0", "\u00B0");
|
||||
System.IO.File.WriteAllText(path, text);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSinglePartFileName(ModelDoc2 model, string equipment)
|
||||
{
|
||||
var title = model.GetTitle().Replace(".SLDPRT", "");
|
||||
var config = model.ConfigurationManager.ActiveConfiguration.Name;
|
||||
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var name = isDefaultConfig ? title : $"{title} [{config}]";
|
||||
return prefix + name;
|
||||
return string.IsNullOrWhiteSpace(equipment) ? name : $"{equipment} {name}";
|
||||
}
|
||||
|
||||
private string GetItemFileName(Item item, string prefix)
|
||||
private string GetItemFileName(Item item, ExportContext context)
|
||||
{
|
||||
prefix = prefix?.Replace("\"", "''") ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.ItemNo))
|
||||
if (string.IsNullOrWhiteSpace(context.DrawingNo))
|
||||
{
|
||||
return prefix + item.PartName;
|
||||
// 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');
|
||||
// Expected format: {DrawingNo} PT{ItemNo}
|
||||
// Expected format: {EquipNo} {DrawingNo} PT{ItemNo}
|
||||
return string.IsNullOrWhiteSpace(prefix)
|
||||
? $"PT{num}"
|
||||
: $"{prefix} PT{num}";
|
||||
|
||||
202
ExportDXF/Utilities/ContentHasher.cs
Normal file
202
ExportDXF/Utilities/ContentHasher.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
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 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)
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a SHA256 hash of the entire file contents (for PDFs and other binary files).
|
||||
/// </summary>
|
||||
public static string ComputeFileHash(string filePath)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
using (var stream = File.OpenRead(filePath))
|
||||
{
|
||||
var bytes = sha.ComputeHash(stream);
|
||||
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 ulong ComputeDifferenceHash(Image<Rgba32> image)
|
||||
{
|
||||
// Resize to 9 wide x 8 tall for 8x8 = 64 bit comparisons
|
||||
image.Mutate(ctx => ctx.Resize(9, 8));
|
||||
|
||||
ulong hash = 0;
|
||||
int bit = 0;
|
||||
|
||||
for (int y = 0; y < 8; y++)
|
||||
{
|
||||
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;
|
||||
|
||||
if (leftGray > rightGray)
|
||||
hash |= (1UL << bit);
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static string ComputeGeometricHash(string filePath)
|
||||
{
|
||||
using (var reader = new DxfReader(filePath))
|
||||
{
|
||||
var doc = reader.Read();
|
||||
var signatures = new List<string>();
|
||||
|
||||
foreach (var entity in doc.Entities)
|
||||
{
|
||||
signatures.Add(GetEntitySignature(entity));
|
||||
}
|
||||
|
||||
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)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,6 @@
|
||||
</startup>
|
||||
<appSettings>
|
||||
<add key="MaxBendRadius" value="2.0"/>
|
||||
<add key="ExportOutputFolder" value="C:\ExportDXF\Output"/>
|
||||
<add key="FabWorksApiUrl" value="http://localhost:5206"/>
|
||||
</appSettings>
|
||||
<connectionStrings>
|
||||
<add name="ExportDxfDb"
|
||||
connectionString="Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;"
|
||||
providerName="Microsoft.Data.SqlClient"/>
|
||||
</connectionStrings>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,32 +1,17 @@
|
||||
using ExportDXF.Models;
|
||||
using FabWorks.Core.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Configuration;
|
||||
|
||||
namespace ExportDXF.Data
|
||||
namespace FabWorks.Core.Data
|
||||
{
|
||||
public class ExportDxfDbContext : DbContext
|
||||
public class FabWorksDbContext : DbContext
|
||||
{
|
||||
public DbSet<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 ExportDxfDbContext() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public ExportDxfDbContext(DbContextOptions<ExportDxfDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (!optionsBuilder.IsConfigured)
|
||||
{
|
||||
var connectionString = ConfigurationManager.ConnectionStrings["ExportDxfDb"]?.ConnectionString
|
||||
?? "Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;";
|
||||
optionsBuilder.UseSqlServer(connectionString);
|
||||
}
|
||||
}
|
||||
public FabWorksDbContext(DbContextOptions<FabWorksDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -36,6 +21,9 @@ namespace ExportDXF.Data
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.DrawingNumber).HasMaxLength(100);
|
||||
entity.Property(e => e.Title).HasMaxLength(200);
|
||||
entity.Property(e => e.EquipmentNo).HasMaxLength(50);
|
||||
entity.Property(e => e.DrawingNo).HasMaxLength(50);
|
||||
entity.Property(e => e.SourceFilePath).HasMaxLength(500);
|
||||
entity.Property(e => e.OutputFolder).HasMaxLength(500);
|
||||
entity.Property(e => e.ExportedBy).HasMaxLength(100);
|
||||
@@ -45,6 +33,11 @@ namespace ExportDXF.Data
|
||||
.WithOne(b => b.ExportRecord)
|
||||
.HasForeignKey(b => b.ExportRecordId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(e => e.Drawing)
|
||||
.WithMany(d => d.ExportRecords)
|
||||
.HasForeignKey(e => e.DrawingId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<BomItem>(entity =>
|
||||
@@ -61,14 +54,41 @@ namespace ExportDXF.Data
|
||||
.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>
|
||||
@@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ExportDXF.Data;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
@@ -9,11 +9,11 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ExportDXF.Migrations
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(ExportDxfDbContext))]
|
||||
[Migration("20260214195856_ExtractCutTemplate")]
|
||||
partial class ExtractCutTemplate
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260218171742_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -25,7 +25,7 @@ namespace ExportDXF.Migrations
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -76,7 +76,7 @@ namespace ExportDXF.Migrations
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -99,7 +99,8 @@ namespace ExportDXF.Migrations
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
@@ -115,7 +116,7 @@ namespace ExportDXF.Migrations
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -123,10 +124,18 @@ namespace ExportDXF.Migrations
|
||||
|
||||
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");
|
||||
|
||||
@@ -146,14 +155,74 @@ namespace ExportDXF.Migrations
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord")
|
||||
b.Property<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)
|
||||
@@ -162,23 +231,36 @@ namespace ExportDXF.Migrations
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("ExportDXF.Models.BomItem", "BomItem")
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("ExportDXF.Models.CutTemplate", "BomItemId")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ExportDXF.Data;
|
||||
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 ExportDXF.Migrations
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(ExportDxfDbContext))]
|
||||
partial class ExportDxfDbContextModelSnapshot : ModelSnapshot
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260219134027_AddCutTemplateRevision")]
|
||||
partial class AddCutTemplateRevision
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
@@ -22,7 +25,7 @@ namespace ExportDXF.Migrations
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -73,7 +76,7 @@ namespace ExportDXF.Migrations
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -96,11 +99,15 @@ namespace ExportDXF.Migrations
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
@@ -112,7 +119,7 @@ namespace ExportDXF.Migrations
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -120,10 +127,18 @@ namespace ExportDXF.Migrations
|
||||
|
||||
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");
|
||||
|
||||
@@ -143,14 +158,74 @@ namespace ExportDXF.Migrations
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord")
|
||||
b.Property<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)
|
||||
@@ -159,23 +234,36 @@ namespace ExportDXF.Migrations
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("ExportDXF.Models.BomItem", "BomItem")
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("ExportDXF.Models.CutTemplate", "BomItemId")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.BomItem", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b =>
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
@@ -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>
|
||||
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