Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace JdeScoping.ExcelIO.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for Excel export functionality.
|
||||
/// </summary>
|
||||
public class ExcelExportOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name in appsettings.json.
|
||||
/// </summary>
|
||||
public const string SectionName = "ExcelExport";
|
||||
|
||||
/// <summary>
|
||||
/// Password for protecting the Search Criteria sheet.
|
||||
/// </summary>
|
||||
public string CriteriaSheetPassword { get; set; } = "JDE_SCOPING_TOOL_PASS";
|
||||
|
||||
/// <summary>
|
||||
/// Password for protecting data sheets (Results, MIS Info, Investigation).
|
||||
/// </summary>
|
||||
public string DataSheetPassword { get; set; } = "JDESCOPINGTOOL";
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of rows per Excel sheet.
|
||||
/// </summary>
|
||||
public int MaxRowsPerSheet { get; set; } = 1048576;
|
||||
|
||||
/// <summary>
|
||||
/// Default date format for Excel cells.
|
||||
/// </summary>
|
||||
public string DefaultDateFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to write debug copies to disk.
|
||||
/// </summary>
|
||||
public bool DebugWriteToFile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Directory for debug output files.
|
||||
/// </summary>
|
||||
public string DebugOutputDirectory { get; set; } = "/tmp/lotfinder";
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.ExcelIO;
|
||||
using JdeScoping.ExcelIO.Configuration;
|
||||
using JdeScoping.ExcelIO.Generators;
|
||||
using JdeScoping.ExcelIO.Helpers;
|
||||
using JdeScoping.ExcelIO.Parsing;
|
||||
using JdeScoping.ExcelIO.Templates;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering Excel I/O services.
|
||||
/// </summary>
|
||||
public static class ExcelIODependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Excel I/O services to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddExcelIO(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Bind options
|
||||
services.Configure<ExcelExportOptions>(
|
||||
configuration.GetSection(ExcelExportOptions.SectionName));
|
||||
|
||||
// Register export service (scoped - per request)
|
||||
services.AddScoped<IExcelExportService, ExcelExportService>();
|
||||
|
||||
// Register template service (singleton - stateless)
|
||||
services.AddSingleton<IExcelTemplateService, ExcelTemplateService>();
|
||||
|
||||
// Register parser service (singleton - stateless)
|
||||
services.AddSingleton<IExcelParserService, ExcelParserService>();
|
||||
|
||||
// Register generators (scoped - they use options)
|
||||
services.AddScoped<CriteriaSheetGenerator>();
|
||||
|
||||
// Register helpers (singleton - stateless)
|
||||
services.AddSingleton<OutputColumnCache>();
|
||||
services.AddSingleton<AttributeTableWriter>();
|
||||
services.AddSingleton<DataEntryTemplateGenerator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Reflection;
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
using JdeScoping.ExcelIO.Configuration;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
using JdeScoping.ExcelIO.Generators;
|
||||
using JdeScoping.ExcelIO.Models.Reporting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JdeScoping.ExcelIO;
|
||||
|
||||
/// <summary>
|
||||
/// Service for generating Excel export files from search results.
|
||||
/// </summary>
|
||||
public class ExcelExportService : IExcelExportService
|
||||
{
|
||||
private readonly ILogger<ExcelExportService> _logger;
|
||||
private readonly IOptions<ExcelExportOptions> _options;
|
||||
private readonly CriteriaSheetGenerator _criteriaGenerator;
|
||||
private readonly AttributeTableWriter _tableWriter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ExcelExportService class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="options">Excel export options.</param>
|
||||
/// <param name="criteriaGenerator">Criteria sheet generator.</param>
|
||||
/// <param name="tableWriter">Attribute table writer.</param>
|
||||
public ExcelExportService(
|
||||
ILogger<ExcelExportService> logger,
|
||||
IOptions<ExcelExportOptions> options,
|
||||
CriteriaSheetGenerator criteriaGenerator,
|
||||
AttributeTableWriter tableWriter)
|
||||
{
|
||||
_logger = logger;
|
||||
_options = options;
|
||||
_criteriaGenerator = criteriaGenerator;
|
||||
_tableWriter = tableWriter;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> GenerateAsync(object search, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (search is not SearchModel searchModel)
|
||||
{
|
||||
throw new ArgumentException($"Expected {nameof(SearchModel)} but received {search.GetType().Name}", nameof(search));
|
||||
}
|
||||
|
||||
return await GenerateAsync(searchModel, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an Excel file from the provided search model.
|
||||
/// </summary>
|
||||
/// <param name="search">Search model with criteria and results.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Excel file as byte array.</returns>
|
||||
public async Task<byte[]> GenerateAsync(SearchModel search, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var scope = _logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["SearchId"] = search.Id,
|
||||
["SearchName"] = search.Name
|
||||
});
|
||||
|
||||
_logger.LogInformation("Starting Excel export generation");
|
||||
|
||||
// ClosedXML operations are synchronous, wrap in Task.Run for non-blocking
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using var workbook = new XLWorkbook();
|
||||
|
||||
// 1. Always generate Search Criteria sheet (first tab)
|
||||
_logger.LogDebug("Generating Search Criteria sheet");
|
||||
_criteriaGenerator.Generate(workbook, search);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 2. Always generate Search Results sheet (second tab)
|
||||
_logger.LogDebug("Generating Search Results sheet");
|
||||
GenerateResultsSheet(workbook, search.Results);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 3. Conditionally generate MIS Info sheet
|
||||
if (search.ExtractMisData && search.MisResults != null && search.MisResults.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("Generating MIS Info sheet with {Count} records", search.MisResults.Count);
|
||||
GenerateMisInfoSheet(workbook, search.MisResults);
|
||||
}
|
||||
else if (search.ExtractMisData)
|
||||
{
|
||||
_logger.LogWarning("ExtractMisData is true but MisResults is null or empty");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 4. Conditionally generate Investigation sheet
|
||||
if (search.ExtractMisData && search.MisNonMatchResults != null && search.MisNonMatchResults.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("Generating Investigation sheet with {Count} records", search.MisNonMatchResults.Count);
|
||||
GenerateInvestigationSheet(workbook, search.MisNonMatchResults);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Save to byte array
|
||||
using var stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
var result = stream.ToArray();
|
||||
|
||||
_logger.LogInformation("Excel export generation completed. Size: {Size} bytes", result.Length);
|
||||
|
||||
// Optional: write debug copy to disk
|
||||
if (_options.Value.DebugWriteToFile)
|
||||
{
|
||||
WriteDebugCopy(search.Id, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private void GenerateResultsSheet(XLWorkbook workbook, List<SearchResult> results)
|
||||
{
|
||||
var tableAttr = typeof(SearchResult).GetCustomAttribute<OutputTableAttribute>();
|
||||
var tabName = tableAttr?.TabName ?? "Search Results";
|
||||
|
||||
var worksheet = workbook.Worksheets.Add(tabName);
|
||||
var table = _tableWriter.WriteTable(worksheet, 1, 1, results);
|
||||
|
||||
if (table != null)
|
||||
{
|
||||
// Apply protection with editable extension area
|
||||
var lastRow = table.RangeAddress.LastAddress.RowNumber;
|
||||
var lastCol = table.RangeAddress.LastAddress.ColumnNumber;
|
||||
|
||||
WorksheetProtector.UnlockExtensionArea(worksheet, lastRow, lastCol);
|
||||
WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateMisInfoSheet(XLWorkbook workbook, List<MisSearchResult> misResults)
|
||||
{
|
||||
var tableAttr = typeof(MisSearchResult).GetCustomAttribute<OutputTableAttribute>();
|
||||
var tabName = tableAttr?.TabName ?? "MIS Info";
|
||||
|
||||
var worksheet = workbook.Worksheets.Add(tabName);
|
||||
var table = _tableWriter.WriteTable(worksheet, 1, 1, misResults);
|
||||
|
||||
if (table != null)
|
||||
{
|
||||
// Apply protection with editable extension area
|
||||
var lastRow = table.RangeAddress.LastAddress.RowNumber;
|
||||
var lastCol = table.RangeAddress.LastAddress.ColumnNumber;
|
||||
|
||||
WorksheetProtector.UnlockExtensionArea(worksheet, lastRow, lastCol);
|
||||
WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateInvestigationSheet(XLWorkbook workbook, List<MisNonMatchSearchResult> misNonMatchResults)
|
||||
{
|
||||
var tableAttr = typeof(MisNonMatchSearchResult).GetCustomAttribute<OutputTableAttribute>();
|
||||
var tabName = tableAttr?.TabName ?? "Investigation";
|
||||
|
||||
var worksheet = workbook.Worksheets.Add(tabName);
|
||||
var table = _tableWriter.WriteTable(worksheet, 1, 1, misNonMatchResults);
|
||||
|
||||
if (table != null)
|
||||
{
|
||||
// Apply protection with editable extension area
|
||||
var lastRow = table.RangeAddress.LastAddress.RowNumber;
|
||||
var lastCol = table.RangeAddress.LastAddress.ColumnNumber;
|
||||
|
||||
WorksheetProtector.UnlockExtensionArea(worksheet, lastRow, lastCol);
|
||||
WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteDebugCopy(int searchId, byte[] result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = _options.Value.DebugOutputDirectory;
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var debugPath = Path.Combine(
|
||||
directory,
|
||||
$"Search_{searchId}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx");
|
||||
File.WriteAllBytes(debugPath, result);
|
||||
_logger.LogDebug("Debug copy written to {Path}", debugPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to write debug copy");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace JdeScoping.ExcelIO.Formatting;
|
||||
|
||||
/// <summary>
|
||||
/// Excel format constants.
|
||||
/// </summary>
|
||||
public static class ExcelFormats
|
||||
{
|
||||
/// <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>
|
||||
/// Padding factor for criteria sheet columns (15%).
|
||||
/// </summary>
|
||||
public const double CriteriaPaddingFactor = 1.15;
|
||||
|
||||
/// <summary>
|
||||
/// Padding factor for data sheet columns (30%).
|
||||
/// </summary>
|
||||
public const double DataPaddingFactor = 1.30;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ClosedXML.Excel;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Formatting;
|
||||
|
||||
/// <summary>
|
||||
/// Header cell formatting utilities.
|
||||
/// </summary>
|
||||
public static class HeaderFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies header formatting to a cell.
|
||||
/// </summary>
|
||||
/// <param name="cell">The cell to format.</param>
|
||||
/// <param name="text">Optional text to set in the cell.</param>
|
||||
public static void ApplyHeaderFormat(IXLCell cell, string? text = null)
|
||||
{
|
||||
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
||||
cell.Style.Font.Bold = true;
|
||||
cell.Style.Fill.BackgroundColor = XLColor.Gainsboro;
|
||||
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
cell.Value = text;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies header formatting to a range.
|
||||
/// </summary>
|
||||
/// <param name="range">The range to format.</param>
|
||||
/// <param name="text">Optional text to set in the first cell.</param>
|
||||
/// <param name="merge">Whether to merge the range.</param>
|
||||
public static void ApplyHeaderFormat(IXLRange range, string? text = null, bool merge = false)
|
||||
{
|
||||
range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
||||
range.Style.Font.Bold = true;
|
||||
range.Style.Fill.BackgroundColor = XLColor.Gainsboro;
|
||||
|
||||
if (merge)
|
||||
{
|
||||
range.Merge();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
range.FirstCell().Value = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using ClosedXML.Excel;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Formatting;
|
||||
|
||||
/// <summary>
|
||||
/// Worksheet protection utilities.
|
||||
/// </summary>
|
||||
public static class WorksheetProtector
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies standard protection to a worksheet with the given password.
|
||||
/// </summary>
|
||||
/// <param name="worksheet">The worksheet to protect.</param>
|
||||
/// <param name="password">The protection password.</param>
|
||||
public static void ApplyProtection(IXLWorksheet worksheet, string password)
|
||||
{
|
||||
var protection = worksheet.Protect(password);
|
||||
|
||||
// Allow these operations
|
||||
protection.AllowElement(XLSheetProtectionElements.DeleteColumns);
|
||||
protection.AllowElement(XLSheetProtectionElements.AutoFilter);
|
||||
protection.AllowElement(XLSheetProtectionElements.FormatCells);
|
||||
protection.AllowElement(XLSheetProtectionElements.FormatColumns);
|
||||
protection.AllowElement(XLSheetProtectionElements.FormatRows);
|
||||
protection.AllowElement(XLSheetProtectionElements.SelectLockedCells);
|
||||
protection.AllowElement(XLSheetProtectionElements.SelectUnlockedCells);
|
||||
protection.AllowElement(XLSheetProtectionElements.EditObjects);
|
||||
protection.AllowElement(XLSheetProtectionElements.Sort);
|
||||
|
||||
// Note: DeleteRows is NOT allowed (not in AllowElement call)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies criteria sheet protection (simpler, just password).
|
||||
/// </summary>
|
||||
/// <param name="worksheet">The worksheet to protect.</param>
|
||||
/// <param name="password">The protection password.</param>
|
||||
public static void ApplyCriteriaProtection(IXLWorksheet worksheet, string password)
|
||||
{
|
||||
worksheet.Protect(password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a range for user editing beyond the data area.
|
||||
/// </summary>
|
||||
/// <param name="worksheet">The worksheet containing the range.</param>
|
||||
/// <param name="lastDataRow">The last row of data.</param>
|
||||
/// <param name="lastDataCol">The last column of data.</param>
|
||||
/// <param name="extensionRows">Number of rows to unlock beyond data (default 1000).</param>
|
||||
/// <param name="extensionCols">Number of columns to unlock beyond data (default 1000).</param>
|
||||
public static void UnlockExtensionArea(
|
||||
IXLWorksheet worksheet,
|
||||
int lastDataRow,
|
||||
int lastDataCol,
|
||||
int extensionRows = 1000,
|
||||
int extensionCols = 1000)
|
||||
{
|
||||
var extensionRange = worksheet.Range(
|
||||
1, lastDataCol + 1,
|
||||
lastDataRow + extensionRows, lastDataCol + extensionCols);
|
||||
extensionRange.Style.Protection.Locked = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Configuration;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
using JdeScoping.ExcelIO.Models.Reporting;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// Generates the Search Criteria sheet for Excel export.
|
||||
/// </summary>
|
||||
public class CriteriaSheetGenerator
|
||||
{
|
||||
private readonly IOptions<ExcelExportOptions> _options;
|
||||
private readonly AttributeTableWriter _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>
|
||||
public CriteriaSheetGenerator(
|
||||
IOptions<ExcelExportOptions> options,
|
||||
AttributeTableWriter tableWriter)
|
||||
{
|
||||
_options = options;
|
||||
_tableWriter = tableWriter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the Search Criteria sheet.
|
||||
/// </summary>
|
||||
/// <param name="workbook">The workbook to add the sheet to.</param>
|
||||
/// <param name="search">The search model with criteria.</param>
|
||||
public void Generate(XLWorkbook workbook, SearchModel search)
|
||||
{
|
||||
var worksheet = workbook.Worksheets.Add("Search Criteria");
|
||||
var row = 1;
|
||||
|
||||
// Write name and user
|
||||
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(row, 1), "Search Name");
|
||||
worksheet.Cell(row, 2).Value = search.Name;
|
||||
|
||||
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(++row, 1), "User Name");
|
||||
worksheet.Cell(row, 2).Value = search.UserName;
|
||||
|
||||
// Skip row
|
||||
row++;
|
||||
|
||||
// Write timestamps
|
||||
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(++row, 1), "Submit timestamp");
|
||||
worksheet.Cell(row, 2).Value = FormatTimestamp(search.SubmitDt);
|
||||
|
||||
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(++row, 1), "Start timestamp");
|
||||
worksheet.Cell(row, 2).Value = FormatTimestamp(search.StartDt);
|
||||
|
||||
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(++row, 1), "Completed timestamp");
|
||||
worksheet.Cell(row, 2).Value = FormatTimestamp(search.EndDt);
|
||||
|
||||
// Skip row
|
||||
row++;
|
||||
|
||||
// Write timespan filter table
|
||||
var timespanData = new List<TimespanFilter>
|
||||
{
|
||||
new() { MinimumDt = search.MinimumDt, MaximumDt = search.MaximumDt }
|
||||
};
|
||||
var timespanTable = _tableWriter.WriteTable(worksheet, ++row, 1, timespanData);
|
||||
if (timespanTable != null)
|
||||
{
|
||||
row = timespanTable.RangeAddress.LastAddress.RowNumber + 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 4;
|
||||
}
|
||||
|
||||
// Write work order filter table
|
||||
var workOrderTable = _tableWriter.WriteTable(worksheet, row, 1, search.WorkOrderFilter);
|
||||
if (workOrderTable != null)
|
||||
{
|
||||
row = workOrderTable.RangeAddress.LastAddress.RowNumber + 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 4;
|
||||
}
|
||||
|
||||
// Write item number filter table
|
||||
var itemNumberTable = _tableWriter.WriteTable(worksheet, row, 1, search.ItemNumberFilter);
|
||||
if (itemNumberTable != null)
|
||||
{
|
||||
row = itemNumberTable.RangeAddress.LastAddress.RowNumber + 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 4;
|
||||
}
|
||||
|
||||
// Write profit center filter table
|
||||
var profitCenterTable = _tableWriter.WriteTable(worksheet, row, 1, search.ProfitCenterFilter);
|
||||
if (profitCenterTable != null)
|
||||
{
|
||||
row = profitCenterTable.RangeAddress.LastAddress.RowNumber + 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 4;
|
||||
}
|
||||
|
||||
// Write work center filter table
|
||||
var workCenterTable = _tableWriter.WriteTable(worksheet, row, 1, search.WorkCenterFilter);
|
||||
if (workCenterTable != null)
|
||||
{
|
||||
row = workCenterTable.RangeAddress.LastAddress.RowNumber + 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 4;
|
||||
}
|
||||
|
||||
// Write component lot filter table
|
||||
var componentLotTable = _tableWriter.WriteTable(worksheet, row, 1, search.ComponentLotFilter);
|
||||
if (componentLotTable != null)
|
||||
{
|
||||
row = componentLotTable.RangeAddress.LastAddress.RowNumber + 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 4;
|
||||
}
|
||||
|
||||
// Write operator filter table
|
||||
var operatorTable = _tableWriter.WriteTable(worksheet, row, 1, search.OperatorFilter);
|
||||
if (operatorTable != null)
|
||||
{
|
||||
row = operatorTable.RangeAddress.LastAddress.RowNumber + 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 4;
|
||||
}
|
||||
|
||||
// Write item/operation/MIS filter table
|
||||
var itemOpMisTable = _tableWriter.WriteTable(worksheet, row, 1, search.ItemOperationMisFilter);
|
||||
if (itemOpMisTable != null)
|
||||
{
|
||||
row = itemOpMisTable.RangeAddress.LastAddress.RowNumber + 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
row += 4;
|
||||
}
|
||||
|
||||
// Write extract MIS data option
|
||||
var headerRange = worksheet.Range(row, 1, row, 2);
|
||||
HeaderFormatter.ApplyHeaderFormat(headerRange, "Extract MIS data?", merge: true);
|
||||
worksheet.Cell(++row, 1).Value = search.ExtractMisData ? "YES" : "NO";
|
||||
|
||||
// Auto-fit columns with 15% padding
|
||||
for (var column = 1; column <= 4; column++)
|
||||
{
|
||||
worksheet.Column(column).AdjustToContents();
|
||||
worksheet.Column(column).Width *= ExcelFormats.CriteriaPaddingFactor;
|
||||
}
|
||||
|
||||
// Apply protection
|
||||
WorksheetProtector.ApplyCriteriaProtection(worksheet, _options.Value.CriteriaSheetPassword);
|
||||
}
|
||||
|
||||
private static string FormatTimestamp(DateTime? dateTime)
|
||||
{
|
||||
if (!dateTime.HasValue)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"{dateTime.Value:MMM dd, yyyy hh:mm:ss tt} EST";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.ExcelIO.Formatting;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// Generates data entry templates for bulk upload.
|
||||
/// </summary>
|
||||
public class DataEntryTemplateGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a single-column data entry template.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of data items.</typeparam>
|
||||
/// <param name="sourceData">Optional source data to pre-populate.</param>
|
||||
/// <param name="headerText">Header text for the column.</param>
|
||||
/// <returns>The Excel file as a byte array.</returns>
|
||||
public byte[] Generate<T>(IEnumerable<T>? sourceData, string headerText)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Data Entry Template");
|
||||
|
||||
// Header
|
||||
var headerCell = worksheet.Cell(1, 1);
|
||||
HeaderFormatter.ApplyHeaderFormat(headerCell, headerText);
|
||||
worksheet.Column(1).Width = 45;
|
||||
|
||||
// Data (if provided)
|
||||
if (sourceData != null)
|
||||
{
|
||||
var row = 2;
|
||||
foreach (var item in sourceData)
|
||||
{
|
||||
worksheet.Cell(row++, 1).Value = ConvertToXlValue(item);
|
||||
}
|
||||
}
|
||||
|
||||
// All cells as text
|
||||
worksheet.Column(1).Style.NumberFormat.Format = ExcelFormats.StdFormat;
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a multi-column data entry template.
|
||||
/// </summary>
|
||||
/// <param name="sourceData">Optional source data to pre-populate (array of rows, each row is array of values).</param>
|
||||
/// <param name="headers">Header texts for each column.</param>
|
||||
/// <returns>The Excel file as a byte array.</returns>
|
||||
public byte[] Generate(object[][]? sourceData, string[] headers)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Data Entry Template");
|
||||
|
||||
// Headers
|
||||
for (var col = 0; col < headers.Length; col++)
|
||||
{
|
||||
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(1, col + 1), headers[col]);
|
||||
worksheet.Column(col + 1).Width = 65;
|
||||
worksheet.Column(col + 1).Style.NumberFormat.Format = ExcelFormats.StdFormat;
|
||||
}
|
||||
|
||||
// Data
|
||||
if (sourceData != null)
|
||||
{
|
||||
for (var row = 0; row < sourceData.Length; row++)
|
||||
{
|
||||
for (var col = 0; col < sourceData[row].Length; col++)
|
||||
{
|
||||
worksheet.Cell(row + 2, col + 1).Value = ConvertToXlValue(sourceData[row][col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\JdeScoping.Core\JdeScoping.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
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!;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// MIS non-match reporting model (Investigation tab).
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Investigation", TableName = "Investigation")]
|
||||
public class MisNonMatchSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Work order job step work center code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Work Center Code")]
|
||||
public string WorkCenterCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work order unique number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Work Order Number")]
|
||||
public long WorkOrderNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order start date.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 30, HeaderText = "Work Order Start Date", Format = OutputColumnAttribute.DateFormat)]
|
||||
public DateTime WorkOrderStartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order job step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 40, HeaderText = "Job Step Number")]
|
||||
public decimal JobStepNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order job step description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 50, HeaderText = "Function Operation Description")]
|
||||
public string JobStepDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work order job step completion date.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 60, HeaderText = "Job Step End Date", Format = OutputColumnAttribute.DateFormat)]
|
||||
public DateTime? JobStepEndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order job step function code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 70, HeaderText = "Function Code")]
|
||||
public string FunctionCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Was job step added.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 75, HeaderText = "Was Job Step Added?")]
|
||||
public bool WasJobStepAdded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Matched work order job step number (match to original router by work order number, work center code, and function code).
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 76, HeaderText = "Matched Job Step Number")]
|
||||
public decimal? MatchedJobStepNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 80, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work order item description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 90, HeaderText = "Item Description")]
|
||||
public string ItemDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Work order router type.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 100, HeaderText = "Routing Type")]
|
||||
public string RoutingType { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// MIS data reporting model.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "MIS Info", TableName = "MIS_Info")]
|
||||
public class MisSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Item unique number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Operation job step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "MIS Job Step Sequence Number")]
|
||||
public string SequenceNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS unique number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 30, HeaderText = "MIS Number")]
|
||||
public string MisNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS revision ID.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 40, HeaderText = "MIS Revision")]
|
||||
public string RevId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 50, HeaderText = "Item Description")]
|
||||
public string ItemDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS release status.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 60, HeaderText = "MIS Release Status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIS release date.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 70, HeaderText = "MIS Release Date", Format = OutputColumnAttribute.TimestampFormat)]
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Branch unique code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 80, HeaderText = "Branch Code")]
|
||||
public string BranchCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Job step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 90, HeaderText = "Job Step Sequence Number")]
|
||||
public decimal JobStepSequenceNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Job step number for matched F3112Z1 / F3111 record.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 100, HeaderText = "Matched Sequence Number")]
|
||||
public decimal? MatchedSequenceNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the job step was matched to F3112Z1 record.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 110, HeaderText = "Matched to F3112Z1?")]
|
||||
public bool RoutingMatch { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the job step was matched to F3111 record.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 120, HeaderText = "Matched to F3003?")]
|
||||
public bool MasterMatch { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Job step function description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 130, HeaderText = "Function Operation Description")]
|
||||
public string FunctionOperationDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Characteristic number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 140, HeaderText = "Char Number")]
|
||||
public string CharNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Test description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 150, HeaderText = "Test Description", AutoWidth = false, Width = OutputColumnAttribute.WrappedColumnWidth, WrapText = true)]
|
||||
public string TestDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Type of sampling.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 160, HeaderText = "Sampling Type")]
|
||||
public string SamplingType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Sampling selection value.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 170, HeaderText = "Sampling Value")]
|
||||
public string SamplingValue { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Tools and gauges for MIS.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 180, HeaderText = "Tools & Gauges", AutoWidth = false, Width = OutputColumnAttribute.WrappedColumnWidth, WrapText = true)]
|
||||
public string ToolsGauges { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Instructions for MIS.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 190, HeaderText = "Work Instructions", AutoWidth = false, Width = OutputColumnAttribute.WrappedColumnWidth, WrapText = true)]
|
||||
public string WorkInstructions { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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>
|
||||
/// Operator unique JDE address number (not exported to Excel).
|
||||
/// </summary>
|
||||
public long AddressNumber { get; set; }
|
||||
|
||||
/// <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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// Reporting search data model for Excel export.
|
||||
/// </summary>
|
||||
public class SearchModel
|
||||
{
|
||||
/// <summary>
|
||||
/// PK ID of search.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User name of user that created search.
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-friendly name for search.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was submitted.
|
||||
/// </summary>
|
||||
public DateTime? SubmitDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was started.
|
||||
/// </summary>
|
||||
public DateTime? StartDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp search was completed.
|
||||
/// </summary>
|
||||
public DateTime? EndDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum timestamp to include.
|
||||
/// </summary>
|
||||
public DateTime? MinimumDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum timestamp to include.
|
||||
/// </summary>
|
||||
public DateTime? MaximumDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not timespan filter is enabled.
|
||||
/// </summary>
|
||||
public bool TimespanFilterEnabled => MinimumDt.HasValue || MaximumDt.HasValue;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of work order numbers to include.
|
||||
/// </summary>
|
||||
public List<WorkOrderFilterEntry> WorkOrderFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not work order filter is enabled.
|
||||
/// </summary>
|
||||
public bool WorkOrderFilterEnabled => WorkOrderFilter.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of item numbers to include.
|
||||
/// </summary>
|
||||
public List<ItemNumberFilterEntry> ItemNumberFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not item number filter is enabled.
|
||||
/// </summary>
|
||||
public bool ItemNumberFilterEnabled => ItemNumberFilter.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of included profit centers.
|
||||
/// </summary>
|
||||
public List<ProfitCenterFilterEntry> ProfitCenterFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not profit center filter is enabled.
|
||||
/// </summary>
|
||||
public bool ProfitCenterFilterEnabled => ProfitCenterFilter.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of included work centers.
|
||||
/// </summary>
|
||||
public List<WorkCenterFilterEntry> WorkCenterFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not work center filter is enabled.
|
||||
/// </summary>
|
||||
public bool WorkCenterFilterEnabled => WorkCenterFilter.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of included operator IDs.
|
||||
/// </summary>
|
||||
public List<OperatorFilterEntry> OperatorFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not operator filter is enabled.
|
||||
/// </summary>
|
||||
public bool OperatorFilterEnabled => OperatorFilter.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of included upper level lot numbers.
|
||||
/// </summary>
|
||||
public List<ComponentLotFilterEntry> ComponentLotFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not component lot filter is enabled.
|
||||
/// </summary>
|
||||
public bool ComponentLotFilterEnabled => ComponentLotFilter.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// List of part/operation combinations for MIS filtering.
|
||||
/// </summary>
|
||||
public List<ItemOperationMisFilterEntry> ItemOperationMisFilter { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not item/operation/MIS filter is enabled.
|
||||
/// </summary>
|
||||
public bool ItemOperationMisFilterEnabled => ItemOperationMisFilter.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to extract MIS data.
|
||||
/// </summary>
|
||||
public bool ExtractMisData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order search results.
|
||||
/// </summary>
|
||||
public List<SearchResult> Results { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// MIS results.
|
||||
/// </summary>
|
||||
public List<MisSearchResult>? MisResults { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MIS no match found results.
|
||||
/// </summary>
|
||||
public List<MisNonMatchSearchResult>? MisNonMatchResults { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using JdeScoping.ExcelIO.Attributes;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Models.Reporting;
|
||||
|
||||
/// <summary>
|
||||
/// JDE search result reporting model.
|
||||
/// </summary>
|
||||
[OutputTable(TabName = "Search Results", TableName = "Search_Results")]
|
||||
public class SearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Order unique number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 10, HeaderText = "Work Order Number")]
|
||||
public long WorkOrderNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Order branch code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 20, HeaderText = "Work Order Branch Code")]
|
||||
public string WorkOrderBranchCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Order lot number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 30, HeaderText = "Lot Number")]
|
||||
public string LotNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Order item number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 40, HeaderText = "Item Number")]
|
||||
public string ItemNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item master planning family.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 50, HeaderText = "Planning Family")]
|
||||
public string PlanningFamily { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item master stocking type.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 55, HeaderText = "Stocking Type")]
|
||||
public string StockingType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Order quantity.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 60, HeaderText = "Order Quantity")]
|
||||
public decimal OrderQuantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quantity on hold.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 70, HeaderText = "Held Quantity")]
|
||||
public decimal HeldQuantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quantity scrapped/cancelled.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 80, HeaderText = "Scrapped Quantity")]
|
||||
public decimal ScrappedQuantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quantity shipped.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 90, HeaderText = "Shipped Quantity")]
|
||||
public decimal ShippedQuantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Operation branch code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 100, HeaderText = "Operation Step Branch Code")]
|
||||
public string StepBranchCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Operation step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 110, HeaderText = "Operation Step")]
|
||||
public decimal StepNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Operation step description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 120, HeaderText = "Operation Step Description")]
|
||||
public string StepDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Function operation description (long text).
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 130, HeaderText = "Function Operation Description")]
|
||||
public string FunctionOperationDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of last update to operation step number.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 140, HeaderText = "Operation Step Update Timestamp", Format = OutputColumnAttribute.TimestampFormat)]
|
||||
public DateTime StepUpdateDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Order status code.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 150, HeaderText = "Status Code")]
|
||||
public string StatusCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Order status description.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 160, HeaderText = "Status Description")]
|
||||
public string StatusDescription { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of last update to order status.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 170, HeaderText = "Status Update Timestamp", Format = OutputColumnAttribute.DateFormat)]
|
||||
public DateTime? StatusUpdateDt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it was manually specified.
|
||||
/// </summary>
|
||||
public bool ManuallySpecified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it was split from a flagged work order.
|
||||
/// </summary>
|
||||
public bool SplitOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it received parts from a flagged work order (CARDEX / F4111).
|
||||
/// </summary>
|
||||
public bool Cardex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it received parts from a flagged work order (parts list / F3111).
|
||||
/// </summary>
|
||||
public bool PartsList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Work order was included because it met the filter criteria.
|
||||
/// </summary>
|
||||
public bool Flagged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason work order was included in results.
|
||||
/// </summary>
|
||||
[OutputColumn(Order = 180, HeaderText = "Inclusion Reason")]
|
||||
public string InclusionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ManuallySpecified)
|
||||
{
|
||||
return "ManuallySpecified";
|
||||
}
|
||||
if (Flagged)
|
||||
{
|
||||
return "Flagged";
|
||||
}
|
||||
if (Cardex && PartsList)
|
||||
{
|
||||
return "ComponentUsage (CARDEX + Parts List)";
|
||||
}
|
||||
if (Cardex && !PartsList)
|
||||
{
|
||||
return "ComponentUsage (CARDEX)";
|
||||
}
|
||||
if (!Cardex && PartsList)
|
||||
{
|
||||
return "ComponentUsage (Parts List)";
|
||||
}
|
||||
if (SplitOrder)
|
||||
{
|
||||
return "Split order";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
using JdeScoping.Core.ViewModels;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// Service for parsing Excel files uploaded by users.
|
||||
/// </summary>
|
||||
public class ExcelParserService : IExcelParserService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public List<long> ParseWorkOrders(Stream fileStream)
|
||||
{
|
||||
using var workbook = new XLWorkbook(fileStream);
|
||||
var worksheet = workbook.Worksheet(1);
|
||||
|
||||
var workOrderNumbers = new List<long>();
|
||||
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1;
|
||||
|
||||
for (var row = 2; row <= lastRow; row++)
|
||||
{
|
||||
var cellValue = worksheet.Cell(row, 1).GetString()?.Trim();
|
||||
if (long.TryParse(cellValue, out var woNumber))
|
||||
{
|
||||
workOrderNumbers.Add(woNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return workOrderNumbers;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<string> ParseItems(Stream fileStream)
|
||||
{
|
||||
using var workbook = new XLWorkbook(fileStream);
|
||||
var worksheet = workbook.Worksheet(1);
|
||||
|
||||
var itemNumbers = new List<string>();
|
||||
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1;
|
||||
|
||||
for (var row = 2; row <= lastRow; row++)
|
||||
{
|
||||
var cellValue = worksheet.Cell(row, 1).GetString()?.Trim();
|
||||
if (!string.IsNullOrEmpty(cellValue))
|
||||
{
|
||||
itemNumbers.Add(cellValue);
|
||||
}
|
||||
}
|
||||
|
||||
return itemNumbers;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<LotViewModel> ParseComponentLots(Stream fileStream)
|
||||
{
|
||||
using var workbook = new XLWorkbook(fileStream);
|
||||
var worksheet = workbook.Worksheet(1);
|
||||
|
||||
var lotViewModels = new List<LotViewModel>();
|
||||
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1;
|
||||
|
||||
for (var row = 2; row <= lastRow; row++)
|
||||
{
|
||||
var lotNumber = worksheet.Cell(row, 1).GetString()?.Trim() ?? string.Empty;
|
||||
var itemNumber = worksheet.Cell(row, 2).GetString()?.Trim() ?? string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(lotNumber))
|
||||
{
|
||||
lotViewModels.Add(new LotViewModel
|
||||
{
|
||||
LotNumber = lotNumber,
|
||||
ItemNumber = itemNumber
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return lotViewModels;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<PartOperationViewModel> ParsePartOperations(Stream fileStream)
|
||||
{
|
||||
using var workbook = new XLWorkbook(fileStream);
|
||||
var worksheet = workbook.Worksheet(1);
|
||||
|
||||
var partOperations = new List<PartOperationViewModel>();
|
||||
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1;
|
||||
|
||||
for (var row = 2; row <= lastRow; row++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var itemNumber = worksheet.Cell(row, 1).GetString()?.Trim() ?? string.Empty;
|
||||
var operationNumber = worksheet.Cell(row, 2).GetString()?.Trim() ?? string.Empty;
|
||||
var misNumber = worksheet.Cell(row, 3).GetString()?.Trim() ?? string.Empty;
|
||||
var misRevision = worksheet.Cell(row, 4).GetString()?.Trim() ?? string.Empty;
|
||||
|
||||
// Remove decimal places from operation number
|
||||
if (!string.IsNullOrEmpty(operationNumber) && operationNumber.Contains('.'))
|
||||
{
|
||||
operationNumber = operationNumber[..operationNumber.IndexOf('.')];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(itemNumber) && !string.IsNullOrEmpty(operationNumber))
|
||||
{
|
||||
partOperations.Add(new PartOperationViewModel
|
||||
{
|
||||
ItemNumber = itemNumber,
|
||||
OperationNumber = operationNumber,
|
||||
MisNumber = misNumber,
|
||||
MisRevision = misRevision
|
||||
});
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid rows
|
||||
}
|
||||
}
|
||||
|
||||
return partOperations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using ClosedXML.Excel;
|
||||
using JdeScoping.Core.Interfaces;
|
||||
|
||||
namespace JdeScoping.ExcelIO.Templates;
|
||||
|
||||
/// <summary>
|
||||
/// Service for generating Excel template files for data entry.
|
||||
/// </summary>
|
||||
public class ExcelTemplateService : IExcelTemplateService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSingleColumn<T>(IEnumerable<T> data, string headerText)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Template");
|
||||
|
||||
// Write header
|
||||
worksheet.Cell(1, 1).Value = headerText;
|
||||
worksheet.Cell(1, 1).Style.Font.Bold = true;
|
||||
|
||||
// Write data
|
||||
var row = 2;
|
||||
foreach (var item in data)
|
||||
{
|
||||
worksheet.Cell(row, 1).Value = item?.ToString() ?? string.Empty;
|
||||
row++;
|
||||
}
|
||||
|
||||
// Auto-fit column width
|
||||
worksheet.Column(1).AdjustToContents();
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateMultiColumn(object?[][] data, string[] headers)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Template");
|
||||
|
||||
// Write headers
|
||||
for (var col = 0; col < headers.Length; col++)
|
||||
{
|
||||
worksheet.Cell(1, col + 1).Value = headers[col];
|
||||
worksheet.Cell(1, col + 1).Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
// Write data
|
||||
for (var row = 0; row < data.Length; row++)
|
||||
{
|
||||
for (var col = 0; col < data[row].Length; col++)
|
||||
{
|
||||
var value = data[row][col];
|
||||
if (value != null)
|
||||
{
|
||||
worksheet.Cell(row + 2, col + 1).Value = value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-fit column widths
|
||||
worksheet.Columns().AdjustToContents();
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user