refactor(ExcelIO): delete old attribute-based infrastructure
- Remove OutputColumnAttribute, OutputTableAttribute, OutputColumnCache - Remove AttributeTableWriter and ColumnFormatter - Remove duplicate ExcelFormats from Mapping (use Formatting version) - Remove OutputColumn model - Add FilterEntryMaps for criteria sheet filter models - Update CriteriaSheetGenerator to use FluentTableWriter - Remove attributes from filter entry models (now use fluent maps) - Update DI to register filter entry maps and remove old services - Update tests to use new fluent infrastructure - Delete obsolete test files for removed infrastructure Task 16 of fluent-excel-mapping-implementation plan.
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
namespace JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Excel output column specification.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class OutputColumnAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard format (text).
|
||||
/// </summary>
|
||||
public const string StdFormat = "@";
|
||||
|
||||
/// <summary>
|
||||
/// Standard date format.
|
||||
/// </summary>
|
||||
public const string DateFormat = "[$-409]MM/dd/yyyy;@";
|
||||
|
||||
/// <summary>
|
||||
/// Standard timestamp format.
|
||||
/// </summary>
|
||||
public const string TimestampFormat = "[$-409]m/d/yy h:mm AM/PM;@";
|
||||
|
||||
/// <summary>
|
||||
/// Wrapped text column default width.
|
||||
/// </summary>
|
||||
public const double WrappedColumnWidth = 65;
|
||||
|
||||
/// <summary>
|
||||
/// Order to display column.
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Override text to display for column header.
|
||||
/// </summary>
|
||||
public string HeaderText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Column format (Excel formatting string).
|
||||
/// </summary>
|
||||
public string Format { get; set; } = StdFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not width should be set automatically.
|
||||
/// </summary>
|
||||
public bool AutoWidth { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Manually set width (only used if AutoWidth = false).
|
||||
/// </summary>
|
||||
public double Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not text should be wrapped.
|
||||
/// </summary>
|
||||
public bool WrapText { get; set; }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Excel output table specification.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class OutputTableAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Output tab name.
|
||||
/// </summary>
|
||||
public string TabName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Table name.
|
||||
/// </summary>
|
||||
public string TableName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not merged header should be shown.
|
||||
/// </summary>
|
||||
public bool ShowHeader { get; set; }
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.ExcelIO;
|
||||
using JdeScoping.ExcelIO.Options;
|
||||
using JdeScoping.ExcelIO.Generators;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
using JdeScoping.ExcelIO.Mapping;
|
||||
using JdeScoping.ExcelIO.Mapping.Maps;
|
||||
using JdeScoping.ExcelIO.Parsing;
|
||||
@@ -42,18 +41,29 @@ public static class ExcelIODependencyInjection
|
||||
// Register generators (scoped - they use options)
|
||||
services.AddScoped<CriteriaSheetGenerator>();
|
||||
|
||||
// Register helpers (singleton - stateless)
|
||||
services.AddSingleton<OutputColumnCache>();
|
||||
services.AddSingleton<AttributeTableWriter>();
|
||||
// Register template generator (singleton - stateless)
|
||||
services.AddSingleton<DataEntryTemplateGenerator>();
|
||||
|
||||
// Register Excel map registry with all maps
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var registry = new ExcelMapRegistry();
|
||||
|
||||
// Search result maps
|
||||
registry.Register(new SearchResultMap());
|
||||
registry.Register(new MisSearchResultMap());
|
||||
registry.Register(new MisNonMatchSearchResultMap());
|
||||
|
||||
// Filter entry maps (for criteria sheet)
|
||||
registry.Register(new TimespanFilterMap());
|
||||
registry.Register(new WorkOrderFilterEntryMap());
|
||||
registry.Register(new ItemNumberFilterEntryMap());
|
||||
registry.Register(new ProfitCenterFilterEntryMap());
|
||||
registry.Register(new WorkCenterFilterEntryMap());
|
||||
registry.Register(new OperatorFilterEntryMap());
|
||||
registry.Register(new ComponentLotFilterEntryMap());
|
||||
registry.Register(new ItemOperationMisFilterEntryMap());
|
||||
|
||||
return registry;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Formatting;
|
||||
|
||||
/// <summary>
|
||||
/// Column width and number format utilities.
|
||||
/// </summary>
|
||||
public static class ColumnFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies column formatting based on attribute settings.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to format.</param>
|
||||
/// <param name="attr">The output column attribute with settings.</param>
|
||||
public static void ApplyColumnFormat(IXLColumn column, OutputColumnAttribute attr)
|
||||
{
|
||||
// Set number format
|
||||
column.Style.NumberFormat.Format = attr.Format;
|
||||
|
||||
// Handle wrap text
|
||||
if (attr.WrapText)
|
||||
{
|
||||
column.Style.Alignment.WrapText = true;
|
||||
}
|
||||
|
||||
// Handle width
|
||||
if (attr.WrapText && !attr.AutoWidth)
|
||||
{
|
||||
// Wrapped columns with fixed width skip auto-fit
|
||||
column.Width = attr.Width;
|
||||
}
|
||||
else if (attr.AutoWidth)
|
||||
{
|
||||
column.AdjustToContents();
|
||||
column.Width *= ExcelFormats.DataPaddingFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
column.Width = attr.Width;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-fits a column with the specified padding factor.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to auto-fit.</param>
|
||||
/// <param name="paddingFactor">The padding factor to apply (e.g., 1.15 for 15% padding).</param>
|
||||
public static void AutoFitWithPadding(IXLColumn column, double paddingFactor)
|
||||
{
|
||||
column.AdjustToContents();
|
||||
column.Width *= paddingFactor;
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
using System.Reflection;
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// Generic attribute-driven table writer for Excel worksheets.
|
||||
/// </summary>
|
||||
public class AttributeTableWriter
|
||||
{
|
||||
private readonly OutputColumnCache _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the AttributeTableWriter class.
|
||||
/// </summary>
|
||||
/// <param name="cache">The output column cache.</param>
|
||||
public AttributeTableWriter(OutputColumnCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a table to the worksheet using attribute-driven column definitions.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of data items.</typeparam>
|
||||
/// <param name="worksheet">The worksheet to write to.</param>
|
||||
/// <param name="startRow">The starting row (1-indexed).</param>
|
||||
/// <param name="startCol">The starting column (1-indexed).</param>
|
||||
/// <param name="data">The data to write.</param>
|
||||
/// <param name="tableNameOverride">Optional table name override.</param>
|
||||
/// <param name="showHeader">Optional override for showing merged header.</param>
|
||||
/// <param name="headerText">Optional header text for merged header.</param>
|
||||
/// <returns>The created table, or null if no data.</returns>
|
||||
public IXLTable? WriteTable<T>(
|
||||
IXLWorksheet worksheet,
|
||||
int startRow,
|
||||
int startCol,
|
||||
IEnumerable<T> data,
|
||||
string? tableNameOverride = null,
|
||||
bool? showHeader = null,
|
||||
string? headerText = null)
|
||||
{
|
||||
var tableAttr = typeof(T).GetCustomAttribute<OutputTableAttribute>();
|
||||
var columns = _cache.GetColumns<T>();
|
||||
var tableName = tableNameOverride ?? tableAttr?.TableName ?? typeof(T).Name;
|
||||
var shouldShowHeader = showHeader ?? (tableAttr?.ShowHeader ?? false);
|
||||
var header = headerText ?? tableAttr?.TabName ?? string.Empty;
|
||||
|
||||
if (columns.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dataList = data.ToList();
|
||||
var baseRow = startRow;
|
||||
|
||||
// Write merged header if requested
|
||||
if (shouldShowHeader && !string.IsNullOrEmpty(header))
|
||||
{
|
||||
var mergedHeaderRange = worksheet.Range(baseRow, startCol, baseRow, startCol + columns.Count - 1);
|
||||
HeaderFormatter.ApplyHeaderFormat(mergedHeaderRange, header, merge: true);
|
||||
baseRow++;
|
||||
}
|
||||
|
||||
// Write column headers
|
||||
var col = startCol;
|
||||
foreach (var column in columns)
|
||||
{
|
||||
var cell = worksheet.Cell(baseRow, col);
|
||||
HeaderFormatter.ApplyHeaderFormat(cell, column.Attribute.HeaderText);
|
||||
|
||||
// Pre-set column formatting
|
||||
worksheet.Column(col).Style.Alignment.WrapText = column.Attribute.WrapText;
|
||||
if (!column.Attribute.AutoWidth)
|
||||
{
|
||||
worksheet.Column(col).Width = column.Attribute.Width;
|
||||
}
|
||||
|
||||
col++;
|
||||
}
|
||||
|
||||
// Write data rows
|
||||
var row = baseRow + 1;
|
||||
foreach (var item in dataList)
|
||||
{
|
||||
col = startCol;
|
||||
foreach (var column in columns)
|
||||
{
|
||||
var value = column.Property.GetValue(item);
|
||||
worksheet.Cell(row, col).Value = ConvertToXlValue(value);
|
||||
col++;
|
||||
}
|
||||
row++;
|
||||
}
|
||||
|
||||
// Handle empty data case - add at least one empty row for valid table
|
||||
if (dataList.Count == 0)
|
||||
{
|
||||
row = baseRow + 1;
|
||||
}
|
||||
|
||||
// Create table range
|
||||
var dataRange = worksheet.Range(
|
||||
baseRow, startCol,
|
||||
baseRow + dataList.Count, startCol + columns.Count - 1);
|
||||
|
||||
// Create table
|
||||
var table = dataRange.CreateTable(tableName);
|
||||
table.Theme = XLTableTheme.TableStyleLight18;
|
||||
table.ShowTotalsRow = false;
|
||||
|
||||
// Apply column formatting and number formats
|
||||
col = startCol;
|
||||
var tableStartRow = table.RangeAddress.FirstAddress.RowNumber;
|
||||
var tableEndRow = table.RangeAddress.LastAddress.RowNumber;
|
||||
|
||||
foreach (var column in columns)
|
||||
{
|
||||
// Apply number format to the column data
|
||||
worksheet.Range(tableStartRow, col, tableEndRow, col)
|
||||
.Style.NumberFormat.Format = column.Attribute.Format;
|
||||
|
||||
// Apply column width
|
||||
if (column.Attribute.WrapText && !column.Attribute.AutoWidth)
|
||||
{
|
||||
worksheet.Column(col).Width = column.Attribute.Width;
|
||||
}
|
||||
else if (column.Attribute.AutoWidth)
|
||||
{
|
||||
worksheet.Column(col).AdjustToContents();
|
||||
worksheet.Column(col).Width *= ExcelFormats.DataPaddingFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
worksheet.Column(col).Width = column.Attribute.Width;
|
||||
}
|
||||
|
||||
col++;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a value to an XLCellValue.
|
||||
/// </summary>
|
||||
private static XLCellValue ConvertToXlValue(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => Blank.Value,
|
||||
string s => s,
|
||||
int i => i,
|
||||
long l => l,
|
||||
decimal d => d,
|
||||
double dbl => dbl,
|
||||
float f => f,
|
||||
DateTime dt => dt,
|
||||
bool b => b,
|
||||
_ => value.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,16 @@ namespace JdeScoping.ExcelIO.Generators;
|
||||
public class CriteriaSheetGenerator
|
||||
{
|
||||
private readonly IOptions<ExcelExportOptions> _options;
|
||||
private readonly AttributeTableWriter _tableWriter;
|
||||
private readonly FluentTableWriter _tableWriter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the CriteriaSheetGenerator class.
|
||||
/// </summary>
|
||||
/// <param name="options">Excel export options.</param>
|
||||
/// <param name="tableWriter">Attribute table writer.</param>
|
||||
/// <param name="tableWriter">Fluent table writer.</param>
|
||||
public CriteriaSheetGenerator(
|
||||
IOptions<ExcelExportOptions> options,
|
||||
AttributeTableWriter tableWriter)
|
||||
FluentTableWriter tableWriter)
|
||||
{
|
||||
_options = options;
|
||||
_tableWriter = tableWriter;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
using JdeScoping.ExcelIO.Models;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Cached reflection for column metadata.
|
||||
/// </summary>
|
||||
public class OutputColumnCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<Type, IReadOnlyList<OutputColumn>> _cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output columns for a given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to get columns for.</typeparam>
|
||||
/// <returns>Ordered list of output columns.</returns>
|
||||
public IReadOnlyList<OutputColumn> GetColumns<T>()
|
||||
{
|
||||
return GetColumns(typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output columns for a given type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to get columns for.</param>
|
||||
/// <returns>Ordered list of output columns.</returns>
|
||||
public IReadOnlyList<OutputColumn> GetColumns(Type type)
|
||||
{
|
||||
return _cache.GetOrAdd(type, BuildColumns);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<OutputColumn> BuildColumns(Type type)
|
||||
{
|
||||
var columns = new List<OutputColumn>();
|
||||
|
||||
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var attr = property.GetCustomAttribute<OutputColumnAttribute>();
|
||||
if (attr != null)
|
||||
{
|
||||
columns.Add(new OutputColumn
|
||||
{
|
||||
Name = property.Name,
|
||||
Property = property,
|
||||
Attribute = attr
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by Order ascending, then by Name alphabetically for tie-breaking
|
||||
return columns
|
||||
.OrderBy(c => c.Attribute.Order)
|
||||
.ThenBy(c => c.Name)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace JdeScoping.ExcelIO.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Standard Excel format strings for column configuration.
|
||||
/// </summary>
|
||||
public static class ExcelFormats
|
||||
{
|
||||
/// <summary>Text format (default).</summary>
|
||||
public const string Text = "@";
|
||||
|
||||
/// <summary>Date format: MM/dd/yyyy</summary>
|
||||
public const string Date = "[$-409]MM/dd/yyyy;@";
|
||||
|
||||
/// <summary>Timestamp format: m/d/yy h:mm AM/PM</summary>
|
||||
public const string Timestamp = "[$-409]m/d/yy h:mm AM/PM;@";
|
||||
|
||||
/// <summary>Default width for wrapped text columns.</summary>
|
||||
public const double WrappedColumnWidth = 65;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
using JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Mapping.Maps;
|
||||
|
||||
/// <summary>
|
||||
/// Excel column mapping for TimespanFilter.
|
||||
/// </summary>
|
||||
public sealed class TimespanFilterMap : ExcelClassMap<TimespanFilter>
|
||||
{
|
||||
public TimespanFilterMap()
|
||||
{
|
||||
Table("Timespan_Filter", "Timespan Filter");
|
||||
|
||||
Map(x => x.MinimumDt).Order(10).Header("Minimum Date").Format(ExcelFormats.DateFormat);
|
||||
Map(x => x.MaximumDt).Order(20).Header("Maximum Date").Format(ExcelFormats.DateFormat);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel column mapping for WorkOrderFilterEntry.
|
||||
/// </summary>
|
||||
public sealed class WorkOrderFilterEntryMap : ExcelClassMap<WorkOrderFilterEntry>
|
||||
{
|
||||
public WorkOrderFilterEntryMap()
|
||||
{
|
||||
Table("Work_Order_Filter", "Work Order Filter");
|
||||
|
||||
Map(x => x.WorkOrderNumber).Order(10).Header("Work Order Number");
|
||||
Map(x => x.ItemNumber).Order(20).Header("Item Number");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel column mapping for ItemNumberFilterEntry.
|
||||
/// </summary>
|
||||
public sealed class ItemNumberFilterEntryMap : ExcelClassMap<ItemNumberFilterEntry>
|
||||
{
|
||||
public ItemNumberFilterEntryMap()
|
||||
{
|
||||
Table("Item_Number_Filter", "Item Number Filter");
|
||||
|
||||
Map(x => x.ItemNumber).Order(10).Header("Item Number");
|
||||
Map(x => x.ItemDescription).Order(20).Header("Item Description");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel column mapping for ProfitCenterFilterEntry.
|
||||
/// </summary>
|
||||
public sealed class ProfitCenterFilterEntryMap : ExcelClassMap<ProfitCenterFilterEntry>
|
||||
{
|
||||
public ProfitCenterFilterEntryMap()
|
||||
{
|
||||
Table("Profit_Center_Filter", "Profit Center Filter");
|
||||
|
||||
Map(x => x.Code).Order(10).Header("Profit Center");
|
||||
Map(x => x.Description).Order(20).Header("Description");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel column mapping for WorkCenterFilterEntry.
|
||||
/// </summary>
|
||||
public sealed class WorkCenterFilterEntryMap : ExcelClassMap<WorkCenterFilterEntry>
|
||||
{
|
||||
public WorkCenterFilterEntryMap()
|
||||
{
|
||||
Table("Work_Center_Filter", "Work Center Filter");
|
||||
|
||||
Map(x => x.Code).Order(10).Header("Work Center");
|
||||
Map(x => x.Description).Order(20).Header("Description");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel column mapping for OperatorFilterEntry.
|
||||
/// </summary>
|
||||
public sealed class OperatorFilterEntryMap : ExcelClassMap<OperatorFilterEntry>
|
||||
{
|
||||
public OperatorFilterEntryMap()
|
||||
{
|
||||
Table("Operator_Filter", "Operator Filter");
|
||||
|
||||
// Note: AddressNumber is not exported to Excel (no OutputColumn attribute in original)
|
||||
Map(x => x.UserId).Order(10).Header("Username");
|
||||
Map(x => x.FullName).Order(20).Header("Name");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel column mapping for ComponentLotFilterEntry.
|
||||
/// </summary>
|
||||
public sealed class ComponentLotFilterEntryMap : ExcelClassMap<ComponentLotFilterEntry>
|
||||
{
|
||||
public ComponentLotFilterEntryMap()
|
||||
{
|
||||
Table("Component_Lot_Filter", "Component Lot Filter");
|
||||
|
||||
Map(x => x.LotNumber).Order(10).Header("Lot Number");
|
||||
Map(x => x.ItemNumber).Order(20).Header("Item Number");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excel column mapping for ItemOperationMisFilterEntry.
|
||||
/// </summary>
|
||||
public sealed class ItemOperationMisFilterEntryMap : ExcelClassMap<ItemOperationMisFilterEntry>
|
||||
{
|
||||
public ItemOperationMisFilterEntryMap()
|
||||
{
|
||||
Table("Item_Operation_MIS_Filter", "Item/Operation/MIS Filter");
|
||||
|
||||
Map(x => x.ItemNumber).Order(10).Header("Item Number");
|
||||
Map(x => x.OperationNumber).Order(20).Header("Operation Number");
|
||||
Map(x => x.MisNumber).Order(30).Header("MIS Number");
|
||||
Map(x => x.MisRevision).Order(40).Header("MIS Revision");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using JdeScoping.Core.Models.SearchResults;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Mapping.Maps;
|
||||
|
||||
@@ -13,10 +14,10 @@ public sealed class MisNonMatchSearchResultMap : ExcelClassMap<MisNonMatchSearch
|
||||
|
||||
Map(x => x.WorkCenterCode).Order(10).Header("Work Center Code");
|
||||
Map(x => x.WorkOrderNumber).Order(20).Header("Work Order Number");
|
||||
Map(x => x.WorkOrderStartDate).Order(30).Header("Work Order Start Date").Format(ExcelFormats.Date);
|
||||
Map(x => x.WorkOrderStartDate).Order(30).Header("Work Order Start Date").Format(ExcelFormats.DateFormat);
|
||||
Map(x => x.JobStepNumber).Order(40).Header("Job Step Number");
|
||||
Map(x => x.JobStepDescription).Order(50).Header("Function Operation Description");
|
||||
Map(x => x.JobStepEndDate).Order(60).Header("Job Step End Date").Format(ExcelFormats.Date);
|
||||
Map(x => x.JobStepEndDate).Order(60).Header("Job Step End Date").Format(ExcelFormats.DateFormat);
|
||||
Map(x => x.FunctionCode).Order(70).Header("Function Code");
|
||||
Map(x => x.WasJobStepAdded).Order(75).Header("Was Job Step Added?");
|
||||
Map(x => x.MatchedJobStepNumber).Order(76).Header("Matched Job Step Number");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using JdeScoping.Core.Models.SearchResults;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Mapping.Maps;
|
||||
|
||||
@@ -17,7 +18,7 @@ public sealed class MisSearchResultMap : ExcelClassMap<MisSearchResult>
|
||||
Map(x => x.RevId).Order(40).Header("MIS Revision");
|
||||
Map(x => x.ItemDescription).Order(50).Header("Item Description");
|
||||
Map(x => x.Status).Order(60).Header("MIS Release Status");
|
||||
Map(x => x.ReleaseDate).Order(70).Header("MIS Release Date").Format(ExcelFormats.Timestamp);
|
||||
Map(x => x.ReleaseDate).Order(70).Header("MIS Release Date").Format(ExcelFormats.TimestampFormat);
|
||||
Map(x => x.BranchCode).Order(80).Header("Branch Code");
|
||||
Map(x => x.JobStepSequenceNumber).Order(90).Header("Job Step Sequence Number");
|
||||
Map(x => x.MatchedSequenceNumber).Order(100).Header("Matched Sequence Number");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using JdeScoping.Core.Models.SearchResults;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Mapping.Maps;
|
||||
|
||||
@@ -25,10 +26,10 @@ public sealed class SearchResultMap : ExcelClassMap<SearchResult>
|
||||
Map(x => x.StepNumber).Order(110).Header("Operation Step");
|
||||
Map(x => x.StepDescription).Order(120).Header("Operation Step Description");
|
||||
Map(x => x.FunctionOperationDescription).Order(130).Header("Function Operation Description");
|
||||
Map(x => x.StepUpdateDt).Order(140).Header("Operation Step Update Timestamp").Format(ExcelFormats.Timestamp);
|
||||
Map(x => x.StepUpdateDt).Order(140).Header("Operation Step Update Timestamp").Format(ExcelFormats.TimestampFormat);
|
||||
Map(x => x.StatusCode).Order(150).Header("Status Code");
|
||||
Map(x => x.StatusDescription).Order(160).Header("Status Description");
|
||||
Map(x => x.StatusUpdateDt).Order(170).Header("Status Update Timestamp").Format(ExcelFormats.Date);
|
||||
Map(x => x.StatusUpdateDt).Order(170).Header("Status Update Timestamp").Format(ExcelFormats.DateFormat);
|
||||
Map(x => x.InclusionReason).Order(180).Header("Inclusion Reason");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Reflection;
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Column metadata model for Excel output.
|
||||
/// </summary>
|
||||
public class OutputColumn
|
||||
{
|
||||
/// <summary>
|
||||
/// Property name.
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Property info for reflection.
|
||||
/// </summary>
|
||||
public PropertyInfo Property { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Output column attribute.
|
||||
/// </summary>
|
||||
public OutputColumnAttribute Attribute { get; init; } = null!;
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Component lot search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Component Lot Filter", ShowHeader = true, TableName = "Component_Lot_Filter")]
|
||||
public class ComponentLotFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Component lot number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Lot Number")]
|
||||
public string LotNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Component lot item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Item number search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Item Number Filter", ShowHeader = true, TableName = "Item_Number_Filter")]
|
||||
public class ItemNumberFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Item Description")]
|
||||
public string ItemDescription { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Item/Operation/MIS search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Item/Operation/MIS Filter", ShowHeader = true, TableName = "Item_Operation_MIS_Filter")]
|
||||
public class ItemOperationMisFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Part's item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Operation's job step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Operation Number")]
|
||||
public string OperationNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 30, HeaderText = "MIS Number")]
|
||||
public string MisNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS revision.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 40, HeaderText = "MIS Revision")]
|
||||
public string MisRevision { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Operator search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Operator Filter", ShowHeader = true, TableName = "Operator_Filter")]
|
||||
public class OperatorFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
@@ -16,12 +13,10 @@ public class OperatorFilterEntry
|
||||
/// <summary>
|
||||
/// Operator login user ID.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Username")]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Operator full name (FIRST + LAST).
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Name")]
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Profit center search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Profit Center Filter", ShowHeader = true, TableName = "Profit_Center_Filter")]
|
||||
public class ProfitCenterFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Profit center code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Profit Center")]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Profit center description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Timespan filter entry for criteria sheet.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Timespan Filter", ShowHeader = true, TableName = "Timespan_Filter")]
|
||||
public class TimespanFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum date/time.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Minimum Date", Format = OutputColumnAttribute.DateFormat)]
|
||||
public DateTime? MinimumDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum date/time.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Maximum Date", Format = OutputColumnAttribute.DateFormat)]
|
||||
public DateTime? MaximumDt { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Work center search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Work Center Filter", ShowHeader = true, TableName = "Work_Center_Filter")]
|
||||
public class WorkCenterFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Work center code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Work Center")]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work center description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Work order search filter entry.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Work Order Filter", ShowHeader = true, TableName = "Work_Order_Filter")]
|
||||
public class WorkOrderFilterEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Work order number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Work Order Number")]
|
||||
public long WorkOrderNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
using JdeScoping.ExcelIO.Generators;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Tests;
|
||||
|
||||
public class AttributeTableWriterTests
|
||||
{
|
||||
private readonly OutputColumnCache _cache = new();
|
||||
private readonly AttributeTableWriter _writer;
|
||||
|
||||
public AttributeTableWriterTests()
|
||||
{
|
||||
_writer = new AttributeTableWriter(_cache);
|
||||
}
|
||||
|
||||
[OutputTable(TabName = "Test Items", TableName = "Test_Items", ShowHeader = false)]
|
||||
private class TestItem
|
||||
{
|
||||
[OutputColumn(Order = 10, HeaderText = "ID")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[OutputColumn(Order = 20, HeaderText = "Name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[OutputColumn(Order = 30, HeaderText = "Value")]
|
||||
public decimal Value { get; set; }
|
||||
}
|
||||
|
||||
[OutputTable(TabName = "Wrapped Table", TableName = "Wrapped_Table")]
|
||||
private class WrappedItem
|
||||
{
|
||||
[OutputColumn(Order = 10, HeaderText = "Description", WrapText = true, AutoWidth = false, Width = 65)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class NoAttributeItem
|
||||
{
|
||||
public string Data { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_CreatesTableWithCorrectColumns()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<TestItem>
|
||||
{
|
||||
new() { Id = 1, Name = "Item 1", Value = 10.5m },
|
||||
new() { Id = 2, Name = "Item 2", Value = 20.5m }
|
||||
};
|
||||
|
||||
var table = _writer.WriteTable(worksheet, 1, 1, data);
|
||||
|
||||
table.ShouldNotBeNull();
|
||||
table.ColumnCount().ShouldBe(3);
|
||||
table.RowCount().ShouldBe(3); // Header + 2 data rows
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_UsesLight18TableStyle()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
|
||||
|
||||
var table = _writer.WriteTable(worksheet, 1, 1, data);
|
||||
|
||||
table.ShouldNotBeNull();
|
||||
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_SetsColumnHeaders()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
|
||||
|
||||
_writer.WriteTable(worksheet, 1, 1, data);
|
||||
|
||||
worksheet.Cell(1, 1).Value.GetText().ShouldBe("ID");
|
||||
worksheet.Cell(1, 2).Value.GetText().ShouldBe("Name");
|
||||
worksheet.Cell(1, 3).Value.GetText().ShouldBe("Value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_WritesDataRows()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<TestItem>
|
||||
{
|
||||
new() { Id = 1, Name = "Item 1", Value = 10.5m },
|
||||
new() { Id = 2, Name = "Item 2", Value = 20.5m }
|
||||
};
|
||||
|
||||
_writer.WriteTable(worksheet, 1, 1, data);
|
||||
|
||||
worksheet.Cell(2, 1).Value.GetNumber().ShouldBe(1);
|
||||
worksheet.Cell(2, 2).Value.GetText().ShouldBe("Item 1");
|
||||
worksheet.Cell(2, 3).Value.GetNumber().ShouldBe(10.5);
|
||||
worksheet.Cell(3, 1).Value.GetNumber().ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_WithShowHeader_CreatesMergedHeader()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
|
||||
|
||||
_writer.WriteTable(worksheet, 1, 1, data, showHeader: true, headerText: "Test Header");
|
||||
|
||||
// First row should be merged header
|
||||
var headerRange = worksheet.Range(1, 1, 1, 3);
|
||||
headerRange.IsMerged().ShouldBeTrue();
|
||||
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Test Header");
|
||||
|
||||
// Column headers should be on row 2
|
||||
worksheet.Cell(2, 1).Value.GetText().ShouldBe("ID");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_EmptyData_CreatesTableWithHeaderOnly()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<TestItem>();
|
||||
|
||||
var table = _writer.WriteTable(worksheet, 1, 1, data);
|
||||
|
||||
table.ShouldNotBeNull();
|
||||
// Table should exist with headers
|
||||
table.ColumnCount().ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_NoAttributes_ReturnsNull()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<NoAttributeItem> { new() { Data = "Test" } };
|
||||
|
||||
var table = _writer.WriteTable(worksheet, 1, 1, data);
|
||||
|
||||
table.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_WrappedColumn_SetsFixedWidth()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<WrappedItem> { new() { Description = "Long description text" } };
|
||||
|
||||
_writer.WriteTable(worksheet, 1, 1, data);
|
||||
|
||||
worksheet.Column(1).Width.ShouldBe(65);
|
||||
worksheet.Column(1).Style.Alignment.WrapText.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTable_TableNameOverride_UsesProvidedName()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
|
||||
|
||||
var table = _writer.WriteTable(worksheet, 1, 1, data, tableNameOverride: "Custom_Table");
|
||||
|
||||
table.ShouldNotBeNull();
|
||||
table.Name.ShouldBe("Custom_Table");
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Tests;
|
||||
|
||||
public class ColumnFormatterTests
|
||||
{
|
||||
[Fact]
|
||||
public void ApplyColumnFormat_AutoWidth_AdjustsToContents()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
worksheet.Cell(1, 1).Value = "Some Text Value";
|
||||
|
||||
var attr = new OutputColumnAttribute
|
||||
{
|
||||
AutoWidth = true,
|
||||
Format = OutputColumnAttribute.StdFormat
|
||||
};
|
||||
|
||||
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
|
||||
|
||||
// Width should be greater than default after adjustment
|
||||
worksheet.Column(1).Width.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyColumnFormat_FixedWidth_SetsExactWidth()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var attr = new OutputColumnAttribute
|
||||
{
|
||||
AutoWidth = false,
|
||||
Width = 50.0,
|
||||
Format = OutputColumnAttribute.StdFormat
|
||||
};
|
||||
|
||||
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
|
||||
|
||||
worksheet.Column(1).Width.ShouldBe(50.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyColumnFormat_WrapText_EnablesWrapping()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var attr = new OutputColumnAttribute
|
||||
{
|
||||
WrapText = true,
|
||||
AutoWidth = false,
|
||||
Width = 65.0,
|
||||
Format = OutputColumnAttribute.StdFormat
|
||||
};
|
||||
|
||||
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
|
||||
|
||||
worksheet.Column(1).Style.Alignment.WrapText.ShouldBeTrue();
|
||||
worksheet.Column(1).Width.ShouldBe(65.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyColumnFormat_DateFormat_AppliesCorrectFormat()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var attr = new OutputColumnAttribute
|
||||
{
|
||||
AutoWidth = false,
|
||||
Width = 20.0,
|
||||
Format = OutputColumnAttribute.DateFormat
|
||||
};
|
||||
|
||||
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
|
||||
|
||||
worksheet.Column(1).Style.NumberFormat.Format.ShouldBe(OutputColumnAttribute.DateFormat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyColumnFormat_TimestampFormat_AppliesCorrectFormat()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
|
||||
var attr = new OutputColumnAttribute
|
||||
{
|
||||
AutoWidth = false,
|
||||
Width = 25.0,
|
||||
Format = OutputColumnAttribute.TimestampFormat
|
||||
};
|
||||
|
||||
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
|
||||
|
||||
worksheet.Column(1).Style.NumberFormat.Format.ShouldBe(OutputColumnAttribute.TimestampFormat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AutoFitWithPadding_AppliesPaddingFactor()
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Test");
|
||||
worksheet.Cell(1, 1).Value = "Some Text";
|
||||
|
||||
ColumnFormatter.AutoFitWithPadding(worksheet.Column(1), 1.30);
|
||||
|
||||
// Width should be greater than 0 and include padding
|
||||
worksheet.Column(1).Width.ShouldBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Options;
|
||||
using JdeScoping.ExcelIO.Generators;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
using JdeScoping.ExcelIO.Mapping;
|
||||
using JdeScoping.ExcelIO.Mapping.Maps;
|
||||
using JdeScoping.ExcelIO.Models.Reporting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Shouldly;
|
||||
@@ -21,11 +22,25 @@ public class CriteriaSheetGeneratorTests
|
||||
CriteriaSheetPassword = "TestPassword"
|
||||
});
|
||||
|
||||
var cache = new OutputColumnCache();
|
||||
var tableWriter = new AttributeTableWriter(cache);
|
||||
var registry = CreateTestRegistry();
|
||||
var tableWriter = new FluentTableWriter(registry);
|
||||
_generator = new CriteriaSheetGenerator(_options, tableWriter);
|
||||
}
|
||||
|
||||
private static ExcelMapRegistry CreateTestRegistry()
|
||||
{
|
||||
var registry = new ExcelMapRegistry();
|
||||
registry.Register(new TimespanFilterMap());
|
||||
registry.Register(new WorkOrderFilterEntryMap());
|
||||
registry.Register(new ItemNumberFilterEntryMap());
|
||||
registry.Register(new ProfitCenterFilterEntryMap());
|
||||
registry.Register(new WorkCenterFilterEntryMap());
|
||||
registry.Register(new OperatorFilterEntryMap());
|
||||
registry.Register(new ComponentLotFilterEntryMap());
|
||||
registry.Register(new ItemOperationMisFilterEntryMap());
|
||||
return registry;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_CreatesSearchCriteriaSheet()
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Options;
|
||||
using JdeScoping.ExcelIO.Generators;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
using JdeScoping.ExcelIO.Mapping;
|
||||
using JdeScoping.ExcelIO.Mapping.Maps;
|
||||
using JdeScoping.ExcelIO.Models.Reporting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -9,6 +10,10 @@ using NSubstitute;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;
|
||||
using MisSearchResult = JdeScoping.Core.Models.SearchResults.MisSearchResult;
|
||||
using MisNonMatchSearchResult = JdeScoping.Core.Models.SearchResults.MisNonMatchSearchResult;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -29,11 +34,33 @@ public class ExcelExportIntegrationTests
|
||||
DataSheetPassword = "TestDataPass"
|
||||
});
|
||||
|
||||
var cache = new OutputColumnCache();
|
||||
var tableWriter = new AttributeTableWriter(cache);
|
||||
var registry = CreateTestRegistry();
|
||||
var tableWriter = new FluentTableWriter(registry);
|
||||
var criteriaGenerator = new CriteriaSheetGenerator(_options, tableWriter);
|
||||
|
||||
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter);
|
||||
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter, registry);
|
||||
}
|
||||
|
||||
private static ExcelMapRegistry CreateTestRegistry()
|
||||
{
|
||||
var registry = new ExcelMapRegistry();
|
||||
|
||||
// Search result maps
|
||||
registry.Register(new SearchResultMap());
|
||||
registry.Register(new MisSearchResultMap());
|
||||
registry.Register(new MisNonMatchSearchResultMap());
|
||||
|
||||
// Filter entry maps
|
||||
registry.Register(new TimespanFilterMap());
|
||||
registry.Register(new WorkOrderFilterEntryMap());
|
||||
registry.Register(new ItemNumberFilterEntryMap());
|
||||
registry.Register(new ProfitCenterFilterEntryMap());
|
||||
registry.Register(new WorkCenterFilterEntryMap());
|
||||
registry.Register(new OperatorFilterEntryMap());
|
||||
registry.Register(new ComponentLotFilterEntryMap());
|
||||
registry.Register(new ItemOperationMisFilterEntryMap());
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
#region Sheet Count Tests
|
||||
@@ -387,10 +414,13 @@ public class ExcelExportIntegrationTests
|
||||
public async Task GenerateAsync_SearchResults_ContainsCorrectData()
|
||||
{
|
||||
var search = CreateMinimalSearchModel();
|
||||
var searchResult = CreateSampleSearchResult();
|
||||
searchResult.WorkOrderNumber = 99999;
|
||||
searchResult.ItemNumber = "TEST-ITEM-001";
|
||||
searchResult.LotNumber = "LOT-999";
|
||||
var searchResult = new SearchResult
|
||||
{
|
||||
WorkOrderNumber = 99999,
|
||||
ItemNumber = "TEST-ITEM-001",
|
||||
LotNumber = "LOT-999",
|
||||
Flagged = true
|
||||
};
|
||||
search.Results.Add(searchResult);
|
||||
|
||||
var result = await _service.GenerateAsync(search);
|
||||
@@ -408,9 +438,16 @@ public class ExcelExportIntegrationTests
|
||||
[Fact]
|
||||
public async Task GenerateAsync_MisInfo_ContainsCorrectData()
|
||||
{
|
||||
var search = CreateSearchModelWithMisData();
|
||||
search.MisResults![0].ItemNumber = "MIS-ITEM-001";
|
||||
search.MisResults[0].MisNumber = "MIS-12345";
|
||||
var search = CreateMinimalSearchModel();
|
||||
search.ExtractMisData = true;
|
||||
search.MisResults = [
|
||||
new MisSearchResult
|
||||
{
|
||||
ItemNumber = "MIS-ITEM-001",
|
||||
MisNumber = "MIS-12345"
|
||||
}
|
||||
];
|
||||
search.MisNonMatchResults = [];
|
||||
|
||||
var result = await _service.GenerateAsync(search);
|
||||
|
||||
@@ -426,9 +463,16 @@ public class ExcelExportIntegrationTests
|
||||
[Fact]
|
||||
public async Task GenerateAsync_Investigation_ContainsCorrectData()
|
||||
{
|
||||
var search = CreateSearchModelWithMisData();
|
||||
search.MisNonMatchResults![0].WorkOrderNumber = 77777;
|
||||
search.MisNonMatchResults[0].ItemNumber = "INV-ITEM-001";
|
||||
var search = CreateMinimalSearchModel();
|
||||
search.ExtractMisData = true;
|
||||
search.MisResults = [];
|
||||
search.MisNonMatchResults = [
|
||||
new MisNonMatchSearchResult
|
||||
{
|
||||
WorkOrderNumber = 77777,
|
||||
ItemNumber = "INV-ITEM-001"
|
||||
}
|
||||
];
|
||||
|
||||
var result = await _service.GenerateAsync(search);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Options;
|
||||
using JdeScoping.ExcelIO.Generators;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
using JdeScoping.ExcelIO.Mapping;
|
||||
using JdeScoping.ExcelIO.Mapping.Maps;
|
||||
using JdeScoping.ExcelIO.Models.Reporting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -9,6 +10,10 @@ using NSubstitute;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;
|
||||
using MisSearchResult = JdeScoping.Core.Models.SearchResults.MisSearchResult;
|
||||
using MisNonMatchSearchResult = JdeScoping.Core.Models.SearchResults.MisNonMatchSearchResult;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Tests;
|
||||
|
||||
public class ExcelExportServiceTests
|
||||
@@ -26,11 +31,33 @@ public class ExcelExportServiceTests
|
||||
DataSheetPassword = "TestDataPass"
|
||||
});
|
||||
|
||||
var cache = new OutputColumnCache();
|
||||
var tableWriter = new AttributeTableWriter(cache);
|
||||
var registry = CreateTestRegistry();
|
||||
var tableWriter = new FluentTableWriter(registry);
|
||||
var criteriaGenerator = new CriteriaSheetGenerator(_options, tableWriter);
|
||||
|
||||
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter);
|
||||
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter, registry);
|
||||
}
|
||||
|
||||
private static ExcelMapRegistry CreateTestRegistry()
|
||||
{
|
||||
var registry = new ExcelMapRegistry();
|
||||
|
||||
// Search result maps
|
||||
registry.Register(new SearchResultMap());
|
||||
registry.Register(new MisSearchResultMap());
|
||||
registry.Register(new MisNonMatchSearchResultMap());
|
||||
|
||||
// Filter entry maps
|
||||
registry.Register(new TimespanFilterMap());
|
||||
registry.Register(new WorkOrderFilterEntryMap());
|
||||
registry.Register(new ItemNumberFilterEntryMap());
|
||||
registry.Register(new ProfitCenterFilterEntryMap());
|
||||
registry.Register(new WorkCenterFilterEntryMap());
|
||||
registry.Register(new OperatorFilterEntryMap());
|
||||
registry.Register(new ComponentLotFilterEntryMap());
|
||||
registry.Register(new ItemOperationMisFilterEntryMap());
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using JdeScoping.ExcelIO.Models.Reporting;
|
||||
using JdeScoping.Core.Models.SearchResults;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Options;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
using JdeScoping.ExcelIO.Generators;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
using JdeScoping.ExcelIO.Mapping;
|
||||
using JdeScoping.ExcelIO.Mapping.Maps;
|
||||
using JdeScoping.ExcelIO.Models.Reporting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -10,6 +10,11 @@ using NSubstitute;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
using ExcelFormats = JdeScoping.ExcelIO.Formatting.ExcelFormats;
|
||||
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;
|
||||
using MisSearchResult = JdeScoping.Core.Models.SearchResults.MisSearchResult;
|
||||
using MisNonMatchSearchResult = JdeScoping.Core.Models.SearchResults.MisNonMatchSearchResult;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -30,11 +35,33 @@ public class LegacyComparisonTests
|
||||
DataSheetPassword = "JDESCOPINGTOOL"
|
||||
});
|
||||
|
||||
var cache = new OutputColumnCache();
|
||||
var tableWriter = new AttributeTableWriter(cache);
|
||||
var registry = CreateTestRegistry();
|
||||
var tableWriter = new FluentTableWriter(registry);
|
||||
var criteriaGenerator = new CriteriaSheetGenerator(options, tableWriter);
|
||||
|
||||
_service = new ExcelExportService(logger, options, criteriaGenerator, tableWriter);
|
||||
_service = new ExcelExportService(logger, options, criteriaGenerator, tableWriter, registry);
|
||||
}
|
||||
|
||||
private static ExcelMapRegistry CreateTestRegistry()
|
||||
{
|
||||
var registry = new ExcelMapRegistry();
|
||||
|
||||
// Search result maps
|
||||
registry.Register(new SearchResultMap());
|
||||
registry.Register(new MisSearchResultMap());
|
||||
registry.Register(new MisNonMatchSearchResultMap());
|
||||
|
||||
// Filter entry maps
|
||||
registry.Register(new TimespanFilterMap());
|
||||
registry.Register(new WorkOrderFilterEntryMap());
|
||||
registry.Register(new ItemNumberFilterEntryMap());
|
||||
registry.Register(new ProfitCenterFilterEntryMap());
|
||||
registry.Register(new WorkCenterFilterEntryMap());
|
||||
registry.Register(new OperatorFilterEntryMap());
|
||||
registry.Register(new ComponentLotFilterEntryMap());
|
||||
registry.Register(new ItemOperationMisFilterEntryMap());
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
#region Search Results Column Order Tests
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
using JdeScoping.ExcelIO.Mapping;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -23,7 +24,7 @@ public class ExcelClassMapTests
|
||||
|
||||
Map(x => x.Id).Order(10).Header("ID Number");
|
||||
Map(x => x.Name).Order(20).Header("Full Name");
|
||||
Map(x => x.CreatedAt).Order(30).Header("Created").Format(ExcelFormats.Timestamp);
|
||||
Map(x => x.CreatedAt).Order(30).Header("Created").Format(ExcelFormats.TimestampFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ public class ExcelClassMapTests
|
||||
var map = new TestModelMap();
|
||||
var columns = map.Columns;
|
||||
|
||||
columns[2].Format.ShouldBe(ExcelFormats.Timestamp);
|
||||
columns[2].Format.ShouldBe(ExcelFormats.TimestampFormat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Tests;
|
||||
|
||||
public class OutputColumnCacheTests
|
||||
{
|
||||
private readonly OutputColumnCache _cache = new();
|
||||
|
||||
[OutputTable(TabName = "Test Table", TableName = "Test_Table")]
|
||||
private class TestModel
|
||||
{
|
||||
[OutputColumn(Order = 30, HeaderText = "Column C")]
|
||||
public string ColumnC { get; set; } = string.Empty;
|
||||
|
||||
[OutputColumn(Order = 10, HeaderText = "Column A")]
|
||||
public string ColumnA { get; set; } = string.Empty;
|
||||
|
||||
[OutputColumn(Order = 20, HeaderText = "Column B")]
|
||||
public string ColumnB { get; set; } = string.Empty;
|
||||
|
||||
public string NonOutputColumn { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class TieBreakModel
|
||||
{
|
||||
[OutputColumn(Order = 10, HeaderText = "Zebra")]
|
||||
public string Zebra { get; set; } = string.Empty;
|
||||
|
||||
[OutputColumn(Order = 10, HeaderText = "Apple")]
|
||||
public string Apple { get; set; } = string.Empty;
|
||||
|
||||
[OutputColumn(Order = 10, HeaderText = "Mango")]
|
||||
public string Mango { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class EmptyModel
|
||||
{
|
||||
public string NoAttributes { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumns_ReturnsColumnsOrderedByOrderProperty()
|
||||
{
|
||||
var columns = _cache.GetColumns<TestModel>();
|
||||
|
||||
columns.Count.ShouldBe(3);
|
||||
columns[0].Attribute.HeaderText.ShouldBe("Column A");
|
||||
columns[1].Attribute.HeaderText.ShouldBe("Column B");
|
||||
columns[2].Attribute.HeaderText.ShouldBe("Column C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumns_TieBreaksAlphabeticallyByPropertyName()
|
||||
{
|
||||
var columns = _cache.GetColumns<TieBreakModel>();
|
||||
|
||||
columns.Count.ShouldBe(3);
|
||||
// All have Order=10, so should be sorted by property name
|
||||
columns[0].Name.ShouldBe("Apple");
|
||||
columns[1].Name.ShouldBe("Mango");
|
||||
columns[2].Name.ShouldBe("Zebra");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumns_ExcludesPropertiesWithoutAttribute()
|
||||
{
|
||||
var columns = _cache.GetColumns<TestModel>();
|
||||
|
||||
columns.Count.ShouldBe(3);
|
||||
columns.ShouldNotContain(c => c.Name == "NonOutputColumn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumns_ReturnsEmptyForEmptyModel()
|
||||
{
|
||||
var columns = _cache.GetColumns<EmptyModel>();
|
||||
|
||||
columns.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumns_CachesResults()
|
||||
{
|
||||
var columns1 = _cache.GetColumns<TestModel>();
|
||||
var columns2 = _cache.GetColumns<TestModel>();
|
||||
|
||||
ReferenceEquals(columns1, columns2).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetColumns_ByType_ReturnsCorrectColumns()
|
||||
{
|
||||
var columns = _cache.GetColumns(typeof(TestModel));
|
||||
|
||||
columns.Count.ShouldBe(3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user