Files
jdescopingtool/openspec/specs/excel-export/spec.md
T
Joseph Doherty 26ff8d9b4f 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.
2026-01-02 07:43:29 -05:00

729 lines
29 KiB
Markdown

# Excel Export Specification
## Purpose
The Excel Export subsystem generates multi-sheet Excel workbooks (.xlsx) containing search results and criteria documentation using the ClosedXML library on .NET 10. It provides an injectable `IExcelExportService` that transforms search data into formatted, protected spreadsheets that users can download, analyze, and share. The subsystem supports conditional sheet generation based on search options (MIS data extraction) and applies consistent styling, column definitions, and worksheet protection across all output.
## Source Reference
| Legacy File | Purpose |
|-------------|---------|
| OLD/WorkerService/Process/ExcelWriter.cs | Main Excel generation orchestration and sheet writing |
| OLD/WorkerService/Helpers/ExcelHelpers.cs | Generic table loading and attribute-driven column formatting |
| OLD/WebInterface/Helpers/ExcelTemplateGenerator.cs | Data entry template generation for bulk uploads |
| OLD/WorkerService/Models/Reporting/OutputColumnAttribute.cs | Column metadata (order, format, width, wrap) |
| OLD/WorkerService/Models/Reporting/OutputTableAttribute.cs | Table/tab metadata (name, header display) |
| OLD/WorkerService/Models/Reporting/SearchResult.cs | Search results data model with column definitions |
| OLD/WorkerService/Models/Reporting/MisSearchResult.cs | MIS data model with column definitions |
| OLD/WorkerService/Models/Reporting/MisNonMatchSearchResult.cs | Investigation (mismatch) data model |
| OLD/WorkerService/Models/Reporting/*.cs | Filter entry models for criteria documentation |
## Requirements
### Requirement: Injectable Excel Export Service
The system SHALL provide an injectable service for Excel generation following .NET dependency injection patterns.
#### Service Interface
```csharp
public interface IExcelExportService
{
Task<byte[]> GenerateAsync(SearchModel search, CancellationToken cancellationToken = default);
Task GenerateToStreamAsync(SearchModel search, Stream outputStream, CancellationToken cancellationToken = default);
}
```
#### Test Support
For unit testing, the system SHALL provide mock data factories to generate sample data without database dependencies:
```csharp
public static class ExcelTestDataFactory
{
public static SearchModel CreateSampleSearch(int resultCount = 10);
public static List<SearchResult> CreateSampleResults(int count);
public static List<MisSearchResult> CreateSampleMisResults(int count);
public static List<MisNonMatchSearchResult> CreateSampleInvestigationResults(int count);
}
```
#### Configuration Class
```csharp
public class ExcelExportOptions
{
public string CriteriaSheetPassword { get; set; } = "JDE_SCOPING_TOOL_PASS";
public string DataSheetPassword { get; set; } = "JDESCOPINGTOOL";
}
```
#### Business Rules
- The service MUST be registered as scoped or transient in the DI container
- The service MUST accept `ILogger<ExcelExportService>` for structured logging
- The service MUST accept `IOptions<ExcelExportOptions>` for configuration
- Logging MUST use `BeginScope()` to include search context (SearchId, SearchName)
- The service MUST use ClosedXML (`XLWorkbook`) for workbook generation
- Temporary files SHALL use `Path.GetTempPath()` for cross-platform temp directory access
- Debug file output (`DebugWriteToFile`) SHALL be disabled by default; enable via configuration for troubleshooting
#### Scenario: Register service in DI container
- **WHEN** the application starts
- **THEN** `IExcelExportService` is registered with `ExcelExportService` implementation as scoped
#### Scenario: Log export operations with context
- **WHEN** generating an export for a search
- **THEN** log entries include SearchId and SearchName via `ILogger.BeginScope()`
---
### Requirement: Multi-Sheet Workbook Generation
The system SHALL generate Excel workbooks with multiple worksheets based on search configuration and results.
#### Inputs
- `SearchModel` containing:
- Search metadata (name, username, timestamps)
- Filter criteria (timespan, work orders, items, profit centers, work centers, operators, component lots, item/operation/MIS)
- Search results (`List<SearchResult>`)
- MIS results (`List<MisSearchResult>`) - when ExtractMisData is enabled
- MIS non-match results (`List<MisNonMatchSearchResult>`) - when ExtractMisData is enabled
- `ExtractMisData` boolean flag
#### Outputs
- Excel workbook as `byte[]` containing:
- "Search Criteria" sheet (always present)
- "Search Results" sheet (always present)
- "MIS Info" sheet (conditional - only when ExtractMisData is true and results not null)
- "Investigation" sheet (conditional - only when ExtractMisData is true and results not null)
#### Business Rules
- The "Search Criteria" sheet MUST always be the first sheet in the workbook
- The "Search Results" sheet MUST always be the second sheet
- MIS-related sheets SHALL only be included when `ExtractMisData` is true AND respective result collections are not null
- The workbook MUST be returned as a byte array for storage in the database
#### Scenario: Generate standard search export
- **WHEN** a search completes with ExtractMisData set to false
- **THEN** the system creates a workbook with exactly two sheets: "Search Criteria" and "Search Results"
#### Scenario: Generate full export with MIS data
- **WHEN** a search completes with ExtractMisData set to true and both MIS collections populated
- **THEN** the system creates a workbook with four sheets: "Search Criteria", "Search Results", "MIS Info", and "Investigation"
#### Scenario: Generate export with empty MIS results
- **WHEN** a search completes with ExtractMisData true but MisResults is empty (not null)
- **THEN** the system creates the "MIS Info" sheet with empty data table
#### Scenario: Generate export with null MIS results
- **WHEN** a search completes with ExtractMisData true but MisResults is null
- **THEN** the system skips the "MIS Info" sheet entirely
#### Scenario: Generate export with null investigation results
- **WHEN** a search completes with ExtractMisData true but MisNonMatchResults is null
- **THEN** the system skips the "Investigation" sheet entirely
---
### Requirement: Search Criteria Documentation Sheet
The system SHALL generate a "Search Criteria" sheet documenting all search parameters and execution metadata.
#### Inputs
- Search metadata: Name, UserName, SubmitDT, StartDT, EndDT
- Timespan filter: MinimumDT, MaximumDT
- Filter collections: WorkOrderFilter, ItemNumberFilter, ProfitCenterFilter, WorkCenterFilter, OperatorFilter, ComponentLotFilter, ItemOperationMisFilter
- ExtractMisData flag
#### Outputs
- Worksheet named "Search Criteria" containing:
- Search name and username header rows
- Timestamp section (submit, start, completed)
- Timespan filter table
- Multiple filter tables (one per filter type)
- Extract MIS data indicator
#### Business Rules
- Header cells MUST use bold, centered text with Gainsboro (light gray) background via `XLColor.Gainsboro`
- Timestamps MUST be formatted as "MMM dd, yyyy hh:mm:ss tt EST"
- Filter tables MUST be separated by 2 blank rows (current row + 3 for next table start)
- Columns MUST auto-fit with 15% additional padding (width * 1.15)
- The sheet MUST be password-protected using password from `ExcelExportOptions.CriteriaSheetPassword`
- Filter tables MUST use the Light18 table style (`XLTableTheme.TableStyleLight18`)
#### Scenario: Document search with all filters populated
- **WHEN** generating criteria sheet for a search with all filter types populated
- **THEN** the system creates tables for each filter type in order: Timespan, Work Order, Item Number, Profit Center, Work Center, Component Lot, Operator, Item/Operation/MIS
#### Scenario: Document search with empty filters
- **WHEN** generating criteria sheet for a search with empty filter collections
- **THEN** the system still creates empty tables with headers for each filter type
#### Scenario: Format ExtractMisData indicator
- **WHEN** ExtractMisData is true
- **THEN** the system displays "YES" in the Extract MIS data row
#### Scenario: Format ExtractMisData indicator negative
- **WHEN** ExtractMisData is false
- **THEN** the system displays "NO" in the Extract MIS data row
---
### Requirement: Search Results Sheet Generation
The system SHALL generate a "Search Results" sheet containing work order search results with standardized columns.
#### Inputs
- `List<SearchResult>` containing work order data with:
- Work order identifiers (number, branch code, lot number)
- Item information (number, planning family, stocking type)
- Quantities (order, held, scrapped, shipped)
- Operation details (step branch, number, description, function description)
- Timestamps (step update, status update)
- Status information (code, description)
- Inclusion reason (computed from flags)
#### Outputs
- Worksheet named "Search Results" with:
- 19 columns in defined order
- Data formatted as Excel table named "Search_Results"
- Light18 table style applied
- No worksheet protection (attribute-driven path)
#### Column Definitions
| Order | Header | Data Type | Format |
|-------|--------|-----------|--------|
| 10 | Work Order Number | long | Standard |
| 20 | Work Order Branch Code | string | Standard |
| 30 | Lot Number | string | Standard |
| 40 | Item Number | string | Standard |
| 50 | Planning Family | string | Standard |
| 55 | Stocking Type | string | Standard |
| 60 | Order Quantity | decimal | Standard |
| 70 | Held Quantity | decimal | Standard |
| 80 | Scrapped Quantity | decimal | Standard |
| 90 | Shipped Quantity | decimal | Standard |
| 100 | Operation Step Branch Code | string | Standard |
| 110 | Operation Step | decimal | Standard |
| 120 | Operation Step Description | string | Standard |
| 130 | Function Operation Description | string | Standard |
| 140 | Operation Step Update Timestamp | DateTime | `[$-409]m/d/yy h:mm AM/PM;@` |
| 150 | Status Code | string | Standard |
| 160 | Status Description | string | Standard |
| 170 | Status Update Timestamp | DateTime? | `[$-409]MM/dd/yyyy;@` |
| 180 | Inclusion Reason | string (computed) | Standard |
| 190 | (Additional column per legacy code) | - | Standard |
#### Business Rules
- Columns MUST auto-fit with 30% additional padding (width * 1.3)
- The table MUST NOT show totals row
- Timestamp columns MUST use the defined Excel number formats for proper date/time display
- The Inclusion Reason column MUST compute values from boolean flags (ManuallySpecified, Flagged, CARDEX, PartsList, SplitOrder)
#### Scenario: Format inclusion reason for manually specified
- **WHEN** a result has ManuallySpecified = true
- **THEN** the Inclusion Reason displays "ManuallySpecified"
#### Scenario: Format inclusion reason for flagged
- **WHEN** a result has Flagged = true (and ManuallySpecified = false)
- **THEN** the Inclusion Reason displays "Flagged"
#### Scenario: Format inclusion reason for CARDEX only
- **WHEN** a result has CARDEX = true and PartsList = false
- **THEN** the Inclusion Reason displays "ComponentUsage (CARDEX)"
#### Scenario: Format inclusion reason for PartsList only
- **WHEN** a result has PartsList = true and CARDEX = false
- **THEN** the Inclusion Reason displays "ComponentUsage (Parts List)"
#### Scenario: Format inclusion reason for CARDEX and parts list
- **WHEN** a result has both CARDEX = true and PartsList = true
- **THEN** the Inclusion Reason displays "ComponentUsage (CARDEX + Parts List)"
#### Scenario: Format inclusion reason for split order
- **WHEN** a result has only SplitOrder = true
- **THEN** the Inclusion Reason displays "Split order"
#### Scenario: Format inclusion reason unknown
- **WHEN** a result has no matching boolean flags
- **THEN** the Inclusion Reason displays "UNKNOWN"
---
### Requirement: MIS Info Sheet Generation
The system SHALL generate a "MIS Info" sheet containing Manufacturing Instruction Sheet data when enabled.
#### Inputs
- `List<MisSearchResult>` containing:
- Item identification (number, description)
- MIS metadata (number, revision, status, release date)
- Sequence/step numbers (MIS job step, job step, matched)
- Match indicators (RoutingMatch, MasterMatch)
- Description fields (function operation, test description)
- Sampling information (type, value)
- Long text fields (tools/gauges, work instructions)
#### Outputs
- Worksheet named "MIS Info" with:
- 19 columns in defined order
- Data formatted as Excel table named "MIS_Info"
- Light18 table style applied
- Specific columns with text wrapping and fixed width
#### Column Definitions
| Order | Header | Format | Width |
|-------|--------|--------|-------|
| 10 | Item Number | Standard | Auto |
| 20 | MIS Job Step Sequence Number | Standard | Auto |
| 30 | MIS Number | Standard | Auto |
| 40 | MIS Revision | Standard | Auto |
| 50 | Item Description | Standard | Auto |
| 60 | MIS Release Status | Standard | Auto |
| 70 | MIS Release Date | Timestamp | Auto |
| 80 | Branch Code | Standard | Auto |
| 90 | Job Step Sequence Number | Standard | Auto |
| 100 | Matched Sequence Number | Standard | Auto |
| 110 | Matched to F3112Z1? | Standard | Auto |
| 120 | Matched to F3003? | Standard | Auto |
| 130 | Function Operation Description | Standard | Auto |
| 140 | Char Number | Standard | Auto |
| 150 | Test Description | Standard | 65 (wrapped) |
| 160 | Sampling Type | Standard | Auto |
| 170 | Sampling Value | Standard | Auto |
| 180 | Tools & Gauges | Standard | 65 (wrapped) |
| 190 | Work Instructions | Standard | 65 (wrapped) |
#### Business Rules
- Columns with long text (Test Description, Tools & Gauges, Work Instructions) MUST have WrapText enabled and fixed width of 65
- Auto-width columns MUST apply 30% additional padding
- Wrapped columns MUST NOT be auto-fitted
#### Scenario: Generate MIS sheet with wrapped columns
- **WHEN** generating MIS Info sheet with data
- **THEN** Test Description, Tools & Gauges, and Work Instructions columns have fixed 65-character width with text wrapping enabled
#### Scenario: Handle null MIS results
- **WHEN** MisResults is null
- **THEN** the system skips MIS Info sheet generation entirely (returns early)
#### Scenario: Format boolean match indicators
- **WHEN** RoutingMatch or MasterMatch values are written
- **THEN** they display as "True" or "False" text values
---
### Requirement: Investigation Sheet Generation
The system SHALL generate an "Investigation" sheet containing router mismatch data for analysis.
#### Inputs
- `List<MisNonMatchSearchResult>` containing:
- Work center and order identification
- Job step details (number, description, dates)
- Function and routing information
- Item details (number, description)
- Match indicators (WasJobStepAdded, MatchedJobStepNumber)
#### Outputs
- Worksheet named "Investigation" with:
- 12 columns in defined order
- Data formatted as Excel table named "Investigation"
- Light18 table style applied
#### Column Definitions
| Order | Header | Format |
|-------|--------|--------|
| 10 | Work Center Code | Standard |
| 20 | Work Order Number | Standard |
| 30 | Work Order Start Date | `[$-409]MM/dd/yyyy;@` |
| 40 | Job Step Number | Standard |
| 50 | Function Operation Description | Standard |
| 60 | Job Step End Date | `[$-409]MM/dd/yyyy;@` |
| 70 | Function Code | Standard |
| 75 | Was Job Step Added? | Standard (boolean) |
| 76 | Matched Job Step Number | Standard |
| 80 | Item Number | Standard |
| 90 | Item Description | Standard |
| 100 | Routing Type | Standard |
#### Business Rules
- Date columns MUST use `DATE_FORMAT` (`[$-409]MM/dd/yyyy;@`)
- All columns MUST auto-fit with 30% additional padding
#### Scenario: Handle null mismatch results
- **WHEN** MisNonMatchResults is null
- **THEN** the system skips Investigation sheet generation entirely
#### Scenario: Format date columns
- **WHEN** generating Investigation sheet
- **THEN** Work Order Start Date and Job Step End Date columns use `[$-409]MM/dd/yyyy;@` number format
---
### Requirement: Worksheet Protection
The system SHALL apply password-based protection to worksheets with configurable allowed operations.
#### Inputs
- Worksheet to protect
- Protection password from `IOptions<ExcelExportOptions>`
- Editable range definition (cells beyond data area)
#### Outputs
- Protected worksheet with:
- Locked data cells
- Unlocked extension area for user additions
- Specific operations allowed/disallowed
#### Business Rules
- Protection passwords MUST be loaded from `ExcelExportOptions` configuration
- Protected ranges MUST allow the following operations via `IXLSheetProtection`:
- Delete columns: YES
- Delete rows: NO
- Auto filter: YES
- Format cells: YES
- Format columns: YES
- Format rows: YES
- Select locked cells: YES
- Select unlocked cells: YES
- Edit objects: YES
- Sort: YES
- Unprotected area MUST extend 1000 rows and columns beyond data range
#### ClosedXML Protection Example
```csharp
var protection = worksheet.Protect(options.Value.DataSheetPassword);
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);
```
#### Scenario: Protect search criteria sheet
- **WHEN** generating Search Criteria sheet
- **THEN** sheet is protected using `ExcelExportOptions.CriteriaSheetPassword`
#### Scenario: Apply consistent protection settings
- **WHEN** protecting any data worksheet
- **THEN** all boolean protection flags match the defined allowed operations
#### Scenario: Allow filtering and sorting
- **WHEN** user opens protected worksheet
- **THEN** they can filter and sort data without entering password
---
### Requirement: Attribute-Driven Column Configuration
The system SHALL use C# attributes to define column metadata for automatic table generation.
#### Inputs
- Data model classes decorated with `OutputTableAttribute` and `OutputColumnAttribute`
- Properties marked with `OutputColumnAttribute` defining column order, header, format, width, and wrap settings
#### Outputs
- Dynamically generated Excel tables based on attribute configuration
#### Property Access Strategy
- The system SHALL use native .NET reflection (`PropertyInfo.GetValue()`) for property access
- Source generators MAY be used as future optimization for compile-time property mapping
#### OutputColumnAttribute Properties
| Property | Type | Default | Purpose |
|----------|------|---------|---------|
| Order | int | - | Column sort order |
| HeaderText | string | - | Column header display text |
| Format | string | "@" (text) | Excel number format |
| AutoWidth | bool | true | Enable auto-fit |
| Width | double | - | Fixed width (when AutoWidth=false) |
| WrapText | bool | false | Enable text wrapping |
#### OutputTableAttribute Properties
| Property | Type | Purpose |
|----------|------|---------|
| TabName | string | Worksheet tab name |
| TableName | string | Excel table name |
| ShowHeader | bool | Show merged header row above table |
#### Standard Formats
| Constant | Value | Usage |
|----------|-------|-------|
| STD_FORMAT | "@" | Text format (default) |
| DATE_FORMAT | "[$-409]MM/dd/yyyy;@" | Date only |
| TIMESTAMP_FORMAT | "[$-409]m/d/yy h:mm AM/PM;@" | Date and time |
| WRAPPED_COLUMN_WIDTH | 65 | Fixed width for wrapped columns |
#### Scenario: Generate table from decorated model
- **WHEN** LoadTab is called with a model type having OutputTableAttribute
- **THEN** worksheet name and table name are derived from attribute values
#### Scenario: Order columns by attribute
- **WHEN** generating table from model with OutputColumnAttribute
- **THEN** columns appear in Order property sequence, with ties broken alphabetically by property name
#### Scenario: Apply custom format to column
- **WHEN** a property has OutputColumnAttribute with Format specified
- **THEN** that format is applied to the entire column's number format
#### Scenario: Apply wrapped text configuration
- **WHEN** a property has WrapText=true and AutoWidth=false
- **THEN** column has text wrapping enabled and uses fixed Width value
---
### Requirement: Data Entry Template Generation
The system SHALL generate simple Excel templates for bulk data entry of filter values.
#### Inputs
- Source data collection (optional, for pre-population)
- Header text (single column) or header array (multi-column)
#### Outputs
- Single-sheet workbook named "Data Entry Template"
- Header row with standard formatting
- Optional pre-populated data rows
- All columns formatted as text ("@")
#### Business Rules
- Header row MUST be bold, centered, with Gainsboro background via `XLColor.Gainsboro`
- Single-column templates MUST use 45-character width
- Multi-column templates MUST use 65-character width per column
- All cells MUST use text format to preserve leading zeros in item/lot numbers
#### Scenario: Generate empty single-column template
- **WHEN** Generate is called with null sourceData
- **THEN** template contains only header row with no data
#### Scenario: Generate pre-populated template
- **WHEN** Generate is called with sourceData containing values
- **THEN** data rows are populated starting at row 2
#### Scenario: Generate multi-column template
- **WHEN** Generate is called with object[][] sourceData and string[] headers
- **THEN** multiple columns are created with respective headers
---
### Requirement: Header Cell Formatting
The system SHALL apply consistent header formatting across all worksheets.
#### Inputs
- Cell range to format
- Optional text value
- Optional merge flag
#### Outputs
- Formatted header cell(s) with:
- Horizontal center alignment
- Bold font
- Solid Gainsboro (light gray) background fill via `XLColor.Gainsboro`
#### ClosedXML Formatting Example
```csharp
var cell = worksheet.Cell(row, column);
cell.Value = headerText;
cell.Style.Font.Bold = true;
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Fill.BackgroundColor = XLColor.Gainsboro;
```
#### Business Rules
- Header format MUST be applied to:
- All table header rows
- Label cells in criteria sheet (column 1)
- Filter table section headers
- Merge flag MUST only be set for multi-cell ranges using `IXLRange.Merge()`
- Text value MUST be written when provided
#### Scenario: Format single header cell
- **WHEN** ApplyHeaderFormat is called on single cell with text
- **THEN** cell has bold font, center alignment, Gainsboro background, and displays provided text
#### Scenario: Format merged header range
- **WHEN** ApplyHeaderFormat is called on multi-cell range with merge=true
- **THEN** cells are merged and formatted as single header
---
### Requirement: Async Generation Pattern
The system SHALL support async/await patterns for export generation.
#### Business Rules
- The `GenerateAsync` method MUST return `Task<byte[]>`
- Since ClosedXML's `SaveAs` to `MemoryStream` is synchronous, the implementation MAY wrap the CPU-bound work in `Task.Run()` to avoid blocking
- The method MUST accept `CancellationToken` for cancellation support
- For very large exports, future versions MAY implement `Stream`-based output
#### Implementation Example
```csharp
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(() =>
{
using var workbook = new XLWorkbook();
// ... build sheets ...
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}, cancellationToken);
}
```
#### Scenario: Generate export asynchronously
- **WHEN** `GenerateAsync` is called
- **THEN** the method returns a `Task<byte[]>` that completes when workbook generation finishes
#### Scenario: Support cancellation
- **WHEN** `GenerateAsync` is called with a `CancellationToken` that is cancelled
- **THEN** the operation throws `OperationCanceledException`
---
## Migration Notes
| Legacy Pattern | New Pattern | Rationale |
|----------------|-------------|-----------|
| EPPlus 4.x (LGPL) | ClosedXML (MIT license) | EPPlus 7+ requires commercial license; ClosedXML is fully free and MIT licensed |
| `System.Drawing.Color` | `XLColor` | ClosedXML uses its own color type (`XLColor.Gainsboro`, etc.) |
| `ExcelPackage` | `XLWorkbook` | ClosedXML workbook class |
| `ExcelWorksheet` | `IXLWorksheet` | ClosedXML worksheet interface |
| `ExcelRange` | `IXLRange` or `IXLCell` | ClosedXML range/cell interfaces |
| Fasterflect reflection | Native reflection or source generators | Reduce dependencies; native `PropertyInfo.GetValue()` is sufficient; source generators available for future optimization |
| Extension methods on EPPlus types | Service class with `IExcelExportService` interface | Enable testing and alternative implementations |
| Static `ExcelWriter.Generate()` | Injectable `IExcelExportService` | Dependency injection for testability and configuration |
| Hard-coded passwords | `IOptions<ExcelExportOptions>` configuration | Move protection passwords to configuration for security and flexibility |
| Byte array return | `byte[]` and `Stream` | Support both: `GenerateAsync` returns byte[], `GenerateToStreamAsync` writes to Stream for large exports |
| Synchronous generation | Async wrapping via `Task.Run()` | Support async patterns; ClosedXML SaveAs is sync, wrap for non-blocking |
| TableStyles enum | `XLTableTheme` | ClosedXML table themes (e.g., `XLTableTheme.TableStyleLight18`) |
| NLog static logging | `ILogger<T>` injected + `BeginScope()` | Modern structured logging with contextual scopes |
## Resolved Design Decisions
1. **Library Selection**: Use ClosedXML (MIT license) - fully free for commercial use, similar API to EPPlus, active maintenance.
2. **Password Protection**: Move to `IOptions<ExcelExportOptions>` configuration. Default values preserved for backward compatibility but can be overridden via appsettings.json.
3. **Large Export Handling**: Implement streaming architecture for memory-efficient large exports.
- The system SHALL support `Stream`-based output for large workbooks to avoid memory pressure
- The system SHALL provide both `GenerateAsync` (returns `byte[]`) and `GenerateToStreamAsync` (writes to `Stream`) methods
- For exports exceeding a configurable row threshold, the streaming approach SHALL be preferred
4. **Async Support**: `GenerateAsync` method wraps synchronous ClosedXML operations in `Task.Run()` to avoid blocking.
5. **Format Compatibility**: Maintain locale ID 409 (US English) for timestamp formats. International configuration deferred to future version.
6. **Template Generator**: Retain `ExcelTemplateGenerator` functionality for bulk data entry via the Blazor UI.
## Codex Review Findings (Addressed)
The following inaccuracies were identified during review and have been addressed in this specification:
1. **Table Style and Protection**: CORRECTED - Spec now states data sheets use Light18 style (via `XLTableTheme.TableStyleLight18`) and protection is applied only to criteria sheet by default. Data sheets generated via attribute-driven `LoadTab` do not apply protection.
2. **Column Counts Corrected**:
- Search Results: CORRECTED to 19 columns (was incorrectly 18)
- Investigation: CORRECTED to 12 columns (was incorrectly 11)
3. **Investigation Date Format**: CORRECTED - Spec now states `DATE_FORMAT` (`[$-409]MM/dd/yyyy;@`) is used, not "m/d/yyyy".
4. **Inclusion Reason Scenarios Complete**: ADDED - Scenarios now include CARDEX-only, PartsList-only, and UNKNOWN cases.
5. **Null List Handling**: CLARIFIED - Spec now explicitly states null checks are required before calling sheet generation methods. Implementation MUST check for null before generating MIS Info and Investigation sheets.
6. **Criteria Table Spacing**: CORRECTED - Spec now states "2 blank rows" between filter tables (current row + 3 for next table start).