feat: add ExcelExportService, LogFileService, and RawBomTableReader
- ExcelExportService: read/write BOM and Cut Templates xlsx with ClosedXML - LogFileService: per-export and app-level log file writing - RawBomTableReader: copy visible SolidWorks BOM table data for Excel output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
139
ExportDXF/Services/ExcelExportService.cs
Normal file
139
ExportDXF/Services/ExcelExportService.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ClosedXML.Excel;
|
||||
using ExportDXF.Models;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
public class ExcelExportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads existing Cut Templates from an xlsx file to compare content hashes.
|
||||
/// Returns empty dictionary if file doesn't exist or has no Cut Templates sheet.
|
||||
/// Key = Item #, Value = (ContentHash, Revision, FileName)
|
||||
/// </summary>
|
||||
public Dictionary<string, (string ContentHash, int Revision, string FileName)> ReadExistingCutTemplates(string xlsxPath)
|
||||
{
|
||||
var result = new Dictionary<string, (string, int, string)>();
|
||||
|
||||
if (!File.Exists(xlsxPath))
|
||||
return result;
|
||||
|
||||
using (var workbook = new XLWorkbook(xlsxPath))
|
||||
{
|
||||
if (!workbook.TryGetWorksheet("Cut Templates", out var ws))
|
||||
return result;
|
||||
|
||||
var lastCol = ws.LastColumnUsed()?.ColumnNumber() ?? 0;
|
||||
var lastRow = ws.LastRowUsed()?.RowNumber() ?? 1;
|
||||
|
||||
if (lastCol == 0 || lastRow <= 1)
|
||||
return result;
|
||||
|
||||
var headers = new Dictionary<string, int>();
|
||||
for (int col = 1; col <= lastCol; col++)
|
||||
{
|
||||
var header = ws.Cell(1, col).GetString();
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
headers[header] = col;
|
||||
}
|
||||
|
||||
if (!headers.ContainsKey("Item #") || !headers.ContainsKey("Content Hash"))
|
||||
return result;
|
||||
|
||||
for (int row = 2; row <= lastRow; row++)
|
||||
{
|
||||
var itemNo = ws.Cell(row, headers["Item #"]).GetString();
|
||||
var hash = ws.Cell(row, headers["Content Hash"]).GetString();
|
||||
var revision = headers.ContainsKey("Revision")
|
||||
? ws.Cell(row, headers["Revision"]).GetValue<int>()
|
||||
: 1;
|
||||
var fileName = headers.ContainsKey("File Name")
|
||||
? ws.Cell(row, headers["File Name"]).GetString()
|
||||
: "";
|
||||
|
||||
if (!string.IsNullOrEmpty(itemNo))
|
||||
result[itemNo] = (hash, revision, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes or updates the xlsx file with BOM and Cut Templates sheets.
|
||||
/// rawBomTable: list of rows where each row is a dictionary of column name → value.
|
||||
/// If null or empty, the BOM sheet is not written (Part/Assembly exports).
|
||||
/// </summary>
|
||||
public void Write(
|
||||
string xlsxPath,
|
||||
List<Dictionary<string, string>> rawBomTable,
|
||||
List<BomItem> bomItems)
|
||||
{
|
||||
using (var workbook = File.Exists(xlsxPath)
|
||||
? new XLWorkbook(xlsxPath)
|
||||
: new XLWorkbook())
|
||||
{
|
||||
WriteBomSheet(workbook, rawBomTable);
|
||||
WriteCutTemplatesSheet(workbook, bomItems);
|
||||
workbook.SaveAs(xlsxPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteBomSheet(XLWorkbook workbook, List<Dictionary<string, string>> rawBomTable)
|
||||
{
|
||||
if (rawBomTable == null || rawBomTable.Count == 0)
|
||||
return;
|
||||
|
||||
if (workbook.TryGetWorksheet("BOM", out _))
|
||||
workbook.Worksheets.Delete("BOM");
|
||||
|
||||
var sheet = workbook.Worksheets.Add("BOM");
|
||||
var columns = rawBomTable[0].Keys.ToList();
|
||||
|
||||
for (int col = 0; col < columns.Count; col++)
|
||||
sheet.Cell(1, col + 1).Value = columns[col];
|
||||
|
||||
for (int row = 0; row < rawBomTable.Count; row++)
|
||||
{
|
||||
for (int col = 0; col < columns.Count; col++)
|
||||
{
|
||||
string value;
|
||||
rawBomTable[row].TryGetValue(columns[col], out value);
|
||||
sheet.Cell(row + 2, col + 1).Value = value ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
sheet.Columns().AdjustToContents();
|
||||
}
|
||||
|
||||
private void WriteCutTemplatesSheet(XLWorkbook workbook, List<BomItem> bomItems)
|
||||
{
|
||||
if (workbook.TryGetWorksheet("Cut Templates", out _))
|
||||
workbook.Worksheets.Delete("Cut Templates");
|
||||
|
||||
var sheet = workbook.Worksheets.Add("Cut Templates");
|
||||
|
||||
var headers = new[] { "Item #", "File Name", "Revision", "Thickness", "K-Factor", "Bend Radius", "Content Hash" };
|
||||
for (int col = 0; col < headers.Length; col++)
|
||||
sheet.Cell(1, col + 1).Value = headers[col];
|
||||
|
||||
int row = 2;
|
||||
foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo))
|
||||
{
|
||||
var ct = item.CutTemplate;
|
||||
sheet.Cell(row, 1).Value = item.ItemNo;
|
||||
sheet.Cell(row, 2).Value = ct.DxfFilePath;
|
||||
sheet.Cell(row, 3).Value = ct.Revision;
|
||||
sheet.Cell(row, 4).Value = ct.Thickness ?? 0;
|
||||
sheet.Cell(row, 5).Value = ct.KFactor ?? 0;
|
||||
sheet.Cell(row, 6).Value = ct.DefaultBendRadius ?? 0;
|
||||
sheet.Cell(row, 7).Value = ct.ContentHash ?? "";
|
||||
row++;
|
||||
}
|
||||
|
||||
sheet.Columns().AdjustToContents();
|
||||
}
|
||||
}
|
||||
}
|
||||
60
ExportDXF/Services/LogFileService.cs
Normal file
60
ExportDXF/Services/LogFileService.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
public class LogFileService : IDisposable
|
||||
{
|
||||
private StreamWriter _exportLog;
|
||||
private static readonly string AppLogPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"ExportDXF", "ExportDXF.log");
|
||||
|
||||
public void StartExportLog(string logFilePath)
|
||||
{
|
||||
_exportLog?.Dispose();
|
||||
|
||||
var dir = Path.GetDirectoryName(logFilePath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
_exportLog = new StreamWriter(logFilePath, append: true);
|
||||
}
|
||||
|
||||
public void Log(string level, string message)
|
||||
{
|
||||
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {message}";
|
||||
|
||||
_exportLog?.WriteLine(line);
|
||||
_exportLog?.Flush();
|
||||
|
||||
WriteAppLog(line);
|
||||
}
|
||||
|
||||
public void LogInfo(string message) => Log("INFO", message);
|
||||
public void LogWarning(string message) => Log("WARNING", message);
|
||||
public void LogError(string message) => Log("ERROR", message);
|
||||
|
||||
private void WriteAppLog(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(AppLogPath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
File.AppendAllText(AppLogPath, line + Environment.NewLine);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort app log — don't fail exports if log write fails
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_exportLog?.Dispose();
|
||||
_exportLog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
ExportDXF/Services/RawBomTableReader.cs
Normal file
48
ExportDXF/Services/RawBomTableReader.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using SolidWorks.Interop.sldworks;
|
||||
|
||||
namespace ExportDXF.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads all visible columns and rows from a SolidWorks BOM table annotation
|
||||
/// as raw string data for direct copy into an Excel sheet.
|
||||
/// </summary>
|
||||
public static class RawBomTableReader
|
||||
{
|
||||
public static List<Dictionary<string, string>> Read(BomTableAnnotation bomTable)
|
||||
{
|
||||
var table = (TableAnnotation)bomTable;
|
||||
var rows = new List<Dictionary<string, string>>();
|
||||
|
||||
int colCount = table.ColumnCount;
|
||||
int rowCount = table.RowCount;
|
||||
|
||||
// Build visible column headers
|
||||
var columns = new List<(int Index, string Header)>();
|
||||
for (int col = 0; col < colCount; col++)
|
||||
{
|
||||
if (table.ColumnHidden[col])
|
||||
continue;
|
||||
|
||||
var header = table.get_Text(0, col)?.Trim() ?? $"Column{col}";
|
||||
columns.Add((col, header));
|
||||
}
|
||||
|
||||
// Read data rows (skip header row 0, skip hidden rows)
|
||||
for (int row = 1; row < rowCount; row++)
|
||||
{
|
||||
if (table.RowHidden[row])
|
||||
continue;
|
||||
|
||||
var rowData = new Dictionary<string, string>();
|
||||
foreach (var (colIdx, header) in columns)
|
||||
{
|
||||
rowData[header] = table.get_Text(row, colIdx)?.Trim() ?? "";
|
||||
}
|
||||
rows.Add(rowData);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user