diff --git a/ExportDXF/Services/DxfExportService.cs b/ExportDXF/Services/DxfExportService.cs index 5f8a67b..b7615b3 100644 --- a/ExportDXF/Services/DxfExportService.cs +++ b/ExportDXF/Services/DxfExportService.cs @@ -1,9 +1,7 @@ -using ExportDXF.ApiClient; using ExportDXF.Extensions; using ExportDXF.ItemExtractors; using ExportDXF.Models; using ExportDXF.Utilities; -using ExportDXF; using SolidWorks.Interop.sldworks; using System; using System.Collections.Generic; @@ -15,42 +13,34 @@ namespace ExportDXF.Services { public interface IDxfExportService { - /// - /// Exports the document specified in the context to DXF format. - /// - /// The export context containing all necessary information. Task ExportAsync(ExportContext context); } - /// - /// Service responsible for orchestrating the export of SolidWorks documents to DXF format. - /// Files are generated locally in a temp directory, then uploaded to the API for storage and versioning. - /// public class DxfExportService : IDxfExportService { private readonly ISolidWorksService _solidWorksService; private readonly IBomExtractor _bomExtractor; private readonly IPartExporter _partExporter; private readonly IDrawingExporter _drawingExporter; - private readonly IFabWorksApiClient _apiClient; + private readonly ExcelExportService _excelExportService; + private readonly LogFileService _logFileService; public DxfExportService( ISolidWorksService solidWorksService, IBomExtractor bomExtractor, IPartExporter partExporter, IDrawingExporter drawingExporter, - IFabWorksApiClient apiClient) + ExcelExportService excelExportService, + LogFileService logFileService) { _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)); - _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); + _excelExportService = excelExportService ?? throw new ArgumentNullException(nameof(excelExportService)); + _logFileService = logFileService ?? throw new ArgumentNullException(nameof(logFileService)); } - /// - /// Exports the document specified in the context to DXF format. - /// public async Task ExportAsync(ExportContext context) { if (context == null) @@ -59,6 +49,28 @@ namespace ExportDXF.Services ValidateContext(context); SetupExportContext(context); + var outputFolder = context.OutputFolder; + if (!Directory.Exists(outputFolder)) + Directory.CreateDirectory(outputFolder); + + var prefix = FilenameTemplateParser.GetPrefix( + context.FilenameTemplate, + context.ActiveDocument.Title); + + var xlsxPath = Path.Combine(outputFolder, $"{prefix}.xlsx"); + var logPath = Path.Combine(outputFolder, $"{prefix}.log"); + + _logFileService.StartExportLog(logPath); + _logFileService.LogInfo($"Export started: {context.ActiveDocument.FilePath}"); + _logFileService.LogInfo($"Template: {context.FilenameTemplate}"); + _logFileService.LogInfo($"Output: {outputFolder}"); + + // Read existing cut templates for revision comparison + var existingTemplates = _excelExportService.ReadExistingCutTemplates(xlsxPath); + + var bomItems = new List(); + List> rawBomTable = null; + var startTime = DateTime.Now; var tempDir = CreateTempWorkDir(); @@ -66,26 +78,39 @@ namespace ExportDXF.Services { _solidWorksService.EnableUserControl(false); - var drawingNumber = ParseDrawingNumber(context); - switch (context.ActiveDocument.DocumentType) { case DocumentType.Part: - await ExportPartAsync(context, tempDir, drawingNumber); + await ExportPartAsync(context, tempDir, outputFolder, existingTemplates, bomItems); break; case DocumentType.Assembly: - await ExportAssemblyAsync(context, tempDir, drawingNumber); + await ExportAssemblyAsync(context, tempDir, outputFolder, existingTemplates, bomItems); break; case DocumentType.Drawing: - await ExportDrawingAsync(context, drawingNumber, tempDir); + rawBomTable = await ExportDrawingAsync(context, tempDir, outputFolder, existingTemplates, bomItems); break; default: LogProgress(context, "Unknown document type.", LogLevel.Error); break; } + + // Write Excel file + _excelExportService.Write(xlsxPath, rawBomTable, bomItems); + _logFileService.LogInfo($"Wrote {Path.GetFileName(xlsxPath)}"); + LogProgress(context, $"Saved {Path.GetFileName(xlsxPath)}"); + } + catch (OperationCanceledException) + { + _logFileService.LogWarning("Export cancelled by user"); + throw; + } + catch (Exception ex) + { + _logFileService.LogError($"Export failed: {ex.Message}"); + throw; } finally { @@ -94,13 +119,19 @@ namespace ExportDXF.Services CleanupTempDir(tempDir); var duration = DateTime.Now - startTime; + _logFileService.LogInfo($"Run time: {duration.ToReadableFormat()}"); LogProgress(context, $"Run time: {duration.ToReadableFormat()}"); } } #region Export Methods by Document Type - private async Task ExportPartAsync(ExportContext context, string tempDir, string drawingNumber) + private async Task ExportPartAsync( + ExportContext context, + string tempDir, + string outputFolder, + Dictionary existingTemplates, + List bomItems) { LogProgress(context, "Active document is a Part"); @@ -111,54 +142,25 @@ namespace ExportDXF.Services return; } - 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 - }; - } - } + item.ItemNo = "1"; + var bomItem = CreateBomItem(item); + PlaceDxfFile(item, context, outputFolder, existingTemplates, bomItem); + bomItems.Add(bomItem); context.BomItemCallback?.Invoke(bomItem); - - if (exportRecord != null) - await SaveBomItemAsync(exportRecord.Id, bomItem, context); } } - private async Task ExportAssemblyAsync(ExportContext context, string tempDir, string drawingNumber) + private async Task ExportAssemblyAsync( + ExportContext context, + string tempDir, + string outputFolder, + Dictionary existingTemplates, + List bomItems) { LogProgress(context, "Active document is an Assembly"); LogProgress(context, "Fetching components..."); @@ -180,31 +182,26 @@ namespace ExportDXF.Services LogProgress(context, $"Found {items.Count} item(s)."); - 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)); + // Assign item numbers + int nextNum = 1; 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++; - } + item.ItemNo = nextNum.ToString(); + nextNum++; } } - await ExportItemsAsync(items, tempDir, context, exportRecord?.Id); + await ExportItemsAsync(items, tempDir, outputFolder, context, existingTemplates, bomItems); } - private async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir) + private async Task>> ExportDrawingAsync( + ExportContext context, + string tempDir, + string outputFolder, + Dictionary existingTemplates, + List bomItems) { LogProgress(context, "Active document is a Drawing"); LogProgress(context, "Finding BOM tables..."); @@ -213,59 +210,35 @@ namespace ExportDXF.Services if (drawing == null) { LogProgress(context, "Failed to get drawing document.", LogLevel.Error); - return; + return null; } + // Read raw BOM table for Excel output + var rawBomTable = new List>(); + var bomTables = drawing.GetBomTables(); + foreach (var table in bomTables) + { + var rows = RawBomTableReader.Read(table); + rawBomTable.AddRange(rows); + } + + // Extract items for DXF export var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback); if (items == null || items.Count == 0) { LogProgress(context, "Error: Bill of materials not found.", LogLevel.Error); - return; + return rawBomTable; } LogProgress(context, $"Found {items.Count} component(s)"); - // Export drawing to PDF in temp dir - _drawingExporter.ExportToPdf(drawing, tempDir, context); + // Export drawing to PDF in output folder + _drawingExporter.ExportToPdf(drawing, outputFolder, context); - // Create export record via API - var exportRecord = await CreateExportRecordAsync(context, drawingNumber); + await ExportItemsAsync(items, tempDir, outputFolder, context, existingTemplates, bomItems); - // Upload PDF to API with versioning - try - { - var pdfs = Directory.GetFiles(tempDir, "*.pdf"); - if (pdfs.Length > 0) - { - var pdfTempPath = pdfs[0]; - var pdfHash = ContentHasher.ComputeFileHash(pdfTempPath); - - var uploadResult = await _apiClient.UploadPdfAsync( - pdfTempPath, - context.Equipment, - context.DrawingNo, - pdfHash, - exportRecord?.Id); - - if (uploadResult != null) - { - if (uploadResult.WasUnchanged) - LogProgress(context, $"PDF unchanged: {uploadResult.FileName}", LogLevel.Info); - else if (uploadResult.IsNewFile) - LogProgress(context, $"Saved PDF: {uploadResult.FileName}", LogLevel.Info); - else - LogProgress(context, $"PDF updated: {uploadResult.FileName}", LogLevel.Info); - } - } - } - catch (Exception ex) - { - LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error); - } - - // Export parts to DXF and save BOM items - await ExportItemsAsync(items, tempDir, context, exportRecord?.Id); + return rawBomTable; } #endregion @@ -274,17 +247,12 @@ namespace ExportDXF.Services private void SetupExportContext(ExportContext context) { - // Set up SolidWorks application reference context.SolidWorksApp = _solidWorksService.GetNativeSldWorks(); if (context.SolidWorksApp == null) - { throw new InvalidOperationException("SolidWorks service is not connected."); - } - // Set up drawing template path context.TemplateDrawing = null; - LogProgress(context, "Export context initialized"); } @@ -292,13 +260,11 @@ namespace ExportDXF.Services { try { - // Clean up template drawing if it was created context.CleanupTemplateDrawing(); } catch (Exception ex) { LogProgress(context, $"Warning: Failed to cleanup template drawing: {ex.Message}", LogLevel.Warning); - // Don't throw - this is cleanup code } } @@ -314,7 +280,6 @@ namespace ExportDXF.Services { TopLevelOnly = false }; - return extractor.GetItems(); } catch (Exception ex) @@ -324,10 +289,17 @@ namespace ExportDXF.Services } } - private async Task ExportItemsAsync(List items, string tempDir, ExportContext context, int? exportRecordId = null) + private async Task ExportItemsAsync( + List items, + string tempDir, + string outputFolder, + ExportContext context, + Dictionary existingTemplates, + List bomItems) { int successCount = 0; int skippedCount = 0; + int unchangedCount = 0; int failureCount = 0; int sortOrder = 0; @@ -341,215 +313,150 @@ namespace ExportDXF.Services try { - // PartExporter will handle template drawing creation through context _partExporter.ExportItem(item, tempDir, context); - // Always create BomItem for every item (sheet metal or not) - var bomItem = new BomItem - { - ExportRecordId = exportRecordId ?? 0, - ItemNo = item.ItemNo ?? "", - PartNo = item.FileName ?? item.PartName ?? "", - SortOrder = sortOrder++, - Qty = item.Quantity, - TotalQty = item.Quantity, - Description = item.Description ?? "", - PartName = item.PartName ?? "", - ConfigurationName = item.Configuration ?? "", - Material = item.Material ?? "" - }; + var bomItem = CreateBomItem(item); + bomItem.SortOrder = sortOrder++; - // Only upload and create CutTemplate if DXF was exported successfully if (!string.IsNullOrEmpty(item.LocalTempPath)) { - successCount++; - - var uploadResult = await UploadDxfAsync(item, context); - if (uploadResult != null) - { - bomItem.CutTemplate = new CutTemplate - { - DxfFilePath = uploadResult.StoredFilePath, - ContentHash = item.ContentHash, - Thickness = item.Thickness > 0 ? item.Thickness : null, - KFactor = item.KFactor > 0 ? item.KFactor : null, - DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null - }; - } + var wasPlaced = PlaceDxfFile(item, context, outputFolder, existingTemplates, bomItem); + if (wasPlaced) + successCount++; + else + unchangedCount++; } else { skippedCount++; } - // Add to UI + bomItems.Add(bomItem); context.BomItemCallback?.Invoke(bomItem); - - // Save BOM item via API if we have an export record - if (exportRecordId.HasValue) - { - await SaveBomItemAsync(exportRecordId.Value, bomItem, context); - } } catch (Exception ex) { LogProgress(context, $"Error exporting item {item.ItemNo}: {ex.Message}", LogLevel.Error); + _logFileService.LogError($"Item {item.ItemNo}: {ex.Message}"); failureCount++; } } - var summary = $"Export complete: {successCount} exported, {skippedCount} skipped"; + var summary = $"Export complete: {successCount} exported"; + if (unchangedCount > 0) + summary += $", {unchangedCount} unchanged"; + if (skippedCount > 0) + summary += $", {skippedCount} skipped (non-sheet-metal)"; if (failureCount > 0) summary += $", {failureCount} failed"; + LogProgress(context, summary, failureCount > 0 ? LogLevel.Warning : LogLevel.Info); - - if (exportRecordId.HasValue) - { - LogProgress(context, $"BOM items saved (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info); - } + _logFileService.LogInfo(summary); } - #endregion - - #region File Upload - - private async Task UploadDxfAsync(Item item, ExportContext context) + /// + /// Places a DXF file in the output folder with revision tracking. + /// Returns true if a new file was written, false if unchanged. + /// + private bool PlaceDxfFile( + Item item, + ExportContext context, + string outputFolder, + Dictionary existingTemplates, + BomItem bomItem) { - try + var baseName = FilenameTemplateParser.Evaluate(context.FilenameTemplate, item); + var contentHash = item.ContentHash; + + int revision = 1; + string dxfFileName; + + // Check existing templates for revision comparison + if (existingTemplates.TryGetValue(item.ItemNo, out var existing)) { - var result = await _apiClient.UploadDxfAsync( - item.LocalTempPath, - context.Equipment, - context.DrawingNo, - item.ItemNo, - item.ContentHash); - - if (result.WasUnchanged) - LogProgress(context, $"DXF unchanged: {result.FileName}", LogLevel.Info); - else if (result.IsNewFile) - LogProgress(context, $"Exported: {result.FileName}", LogLevel.Info); - else - LogProgress(context, $"DXF updated: {result.FileName}", LogLevel.Info); - - return result; - } - catch (Exception ex) - { - LogProgress(context, $"DXF upload failed for {item.FileName}: {ex.Message}", LogLevel.Warning); - return null; - } - } - - #endregion - - #region API Helpers - - private async Task CreateExportRecordAsync(ExportContext context, string drawingNumber) - { - try - { - var dto = await _apiClient.CreateExportAsync( - drawingNumber ?? context.ActiveDocument.Title, - context.Equipment ?? "", - context.DrawingNo ?? "", - context.ActiveDocument.FilePath, - "", // Output folder is now managed by the API - context.Title); - - var record = new ExportRecord + if (existing.ContentHash == contentHash) { - Id = dto.Id, - DrawingNumber = dto.DrawingNumber, - EquipmentNo = dto.EquipmentNo, - DrawingNo = dto.DrawingNo, - SourceFilePath = dto.SourceFilePath, - OutputFolder = dto.OutputFolder, - ExportedAt = dto.ExportedAt, - ExportedBy = dto.ExportedBy - }; + // Unchanged — skip file write, keep existing + dxfFileName = existing.FileName; + revision = existing.Revision; - LogProgress(context, $"Created export record (ID: {record.Id})", LogLevel.Info); - return record; - } - catch (Exception ex) - { - LogProgress(context, $"API error creating export record: {ex.Message}", LogLevel.Error); - return null; - } - } + LogProgress(context, $"Unchanged: {dxfFileName}", LogLevel.Info, item.PartName); + _logFileService.LogInfo($"Unchanged: {dxfFileName}"); - private async Task FindExistingItemNoAsync(int? exportRecordId, string partName, string configurationName) - { - if (!exportRecordId.HasValue) - return null; - - try - { - var existing = await _apiClient.FindExistingBomItemAsync(exportRecordId.Value, partName, configurationName); - return existing?.ItemNo; - } - catch - { - return null; - } - } - - private async Task GetNextItemNumberAsync(string drawingNumber) - { - if (string.IsNullOrEmpty(drawingNumber)) - return "1"; - - try - { - return await _apiClient.GetNextItemNumberAsync(drawingNumber); - } - catch - { - return "1"; - } - } - - private async Task SaveBomItemAsync(int exportRecordId, BomItem bomItem, ExportContext context) - { - try - { - var apiBomItem = new ApiBomItem - { - ItemNo = bomItem.ItemNo, - PartNo = bomItem.PartNo, - SortOrder = bomItem.SortOrder, - Qty = bomItem.Qty, - TotalQty = bomItem.TotalQty, - Description = bomItem.Description, - PartName = bomItem.PartName, - ConfigurationName = bomItem.ConfigurationName, - Material = bomItem.Material - }; - - if (bomItem.CutTemplate != null) - { - apiBomItem.CutTemplate = new ApiCutTemplate + bomItem.CutTemplate = new CutTemplate { - DxfFilePath = bomItem.CutTemplate.DxfFilePath, - ContentHash = bomItem.CutTemplate.ContentHash, - Thickness = bomItem.CutTemplate.Thickness, - KFactor = bomItem.CutTemplate.KFactor, - DefaultBendRadius = bomItem.CutTemplate.DefaultBendRadius + DxfFilePath = dxfFileName, + ContentHash = contentHash, + Revision = revision, + Thickness = item.Thickness > 0 ? item.Thickness : (double?)null, + KFactor = item.KFactor > 0 ? item.KFactor : (double?)null, + DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : (double?)null }; - } - await _apiClient.CreateBomItemAsync(exportRecordId, apiBomItem); + return false; + } + else + { + // Changed — increment revision + revision = existing.Revision + 1; + dxfFileName = GetRevisionFileName(baseName, revision); + + LogProgress(context, $"Updated: {dxfFileName} (Rev{revision})", LogLevel.Info, item.PartName); + _logFileService.LogInfo($"Updated: {dxfFileName} (was {existing.FileName})"); + } } - catch (Exception ex) + else { - LogProgress(context, $"API error saving BOM item: {ex.Message}", LogLevel.Error); + // New item + dxfFileName = $"{baseName}.dxf"; + LogProgress(context, $"Exported: {dxfFileName}", LogLevel.Info, item.PartName); + _logFileService.LogInfo($"Exported: {dxfFileName}"); } + + // Copy from temp to output + var destPath = Path.Combine(outputFolder, dxfFileName); + File.Copy(item.LocalTempPath, destPath, overwrite: true); + + bomItem.CutTemplate = new CutTemplate + { + DxfFilePath = Path.GetFileNameWithoutExtension(dxfFileName), + ContentHash = contentHash, + Revision = revision, + Thickness = item.Thickness > 0 ? item.Thickness : (double?)null, + KFactor = item.KFactor > 0 ? item.KFactor : (double?)null, + DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : (double?)null + }; + + return true; + } + + private BomItem CreateBomItem(Item item) + { + return new BomItem + { + 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 ?? "" + }; } #endregion #region Helper Methods + private string GetRevisionFileName(string baseName, int revision) + { + if (revision <= 1) + return $"{baseName}.dxf"; + return $"{baseName} Rev{revision}.dxf"; + } + private string CreateTempWorkDir() { var path = Path.Combine(Path.GetTempPath(), "ExportDXF-" + Guid.NewGuid().ToString("N")); @@ -570,27 +477,6 @@ namespace ExportDXF.Services } } - private string ParseDrawingNumber(ExportContext context) - { - // Use explicit Equipment/DrawingNo from the UI when available - if (!string.IsNullOrWhiteSpace(context?.Equipment)) - { - return !string.IsNullOrWhiteSpace(context?.DrawingNo) - ? $"{context.Equipment} {context.DrawingNo}" - : context.Equipment; - } - - // Fallback: parse from prefix or document title - var candidate = context?.FilePrefix; - var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate); - if (info == null) - { - var title = context?.ActiveDocument?.Title; - info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title); - } - return info?.ToString(); - } - private void ValidateContext(ExportContext context) { if (context.ActiveDocument == null) @@ -598,6 +484,12 @@ namespace ExportDXF.Services if (context.ProgressCallback == null) throw new ArgumentException("ProgressCallback cannot be null.", nameof(context)); + + if (string.IsNullOrWhiteSpace(context.FilenameTemplate)) + throw new ArgumentException("FilenameTemplate cannot be null or empty.", nameof(context)); + + if (string.IsNullOrWhiteSpace(context.OutputFolder)) + throw new ArgumentException("OutputFolder cannot be null or empty.", nameof(context)); } private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null) diff --git a/ExportDXF/Services/PartExporter.cs b/ExportDXF/Services/PartExporter.cs index 789e9bf..b5a5645 100644 --- a/ExportDXF/Services/PartExporter.cs +++ b/ExportDXF/Services/PartExporter.cs @@ -54,7 +54,7 @@ namespace ExportDXF.Services try { - var fileName = GetSinglePartFileName(model, context.FilePrefix); + var fileName = GetSinglePartFileName(model); var savePath = Path.Combine(saveDirectory, fileName + ".dxf"); // Build result item with metadata @@ -139,7 +139,7 @@ namespace ExportDXF.Services EnrichItemWithMetadata(item, model, part); - var fileName = GetItemFileName(item, context.FilePrefix); + var fileName = GetItemFileName(item); var savePath = Path.Combine(saveDirectory, fileName + ".dxf"); var templateDrawing = context.GetOrCreateTemplateDrawing(); @@ -332,7 +332,7 @@ namespace ExportDXF.Services } } - private string GetSinglePartFileName(ModelDoc2 model, string prefix) + private string GetSinglePartFileName(ModelDoc2 model) { var title = model.GetTitle().Replace(".SLDPRT", ""); var config = model.ConfigurationManager.ActiveConfiguration.Name; @@ -341,17 +341,13 @@ namespace ExportDXF.Services return isDefaultConfig ? title : $"{title} [{config}]"; } - private string GetItemFileName(Item item, string prefix) + private string GetItemFileName(Item item) { if (string.IsNullOrWhiteSpace(item.ItemNo)) - return item.PartName; + return item.PartName ?? "unknown"; - prefix = prefix?.Replace("\"", "''") ?? string.Empty; var num = item.ItemNo.PadLeft(2, '0'); - // Expected format: {DrawingNo} PT{ItemNo} - return string.IsNullOrWhiteSpace(prefix) - ? $"PT{num}" - : $"{prefix} PT{num}"; + return $"PT{num}"; } private void LogExportFailure(Item item, ExportContext context)