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:
2026-04-13 22:14:43 -04:00
parent cf17e71b80
commit c6dde6e217
3 changed files with 247 additions and 0 deletions

View 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();
}
}
}

View 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;
}
}
}

View 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;
}
}
}