26ff8d9b4f
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
1615 lines
48 KiB
Markdown
1615 lines
48 KiB
Markdown
# ExcelIO Consolidation Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Rename ExcelExport to ExcelIO and consolidate all Excel file I/O operations into a single project.
|
|
|
|
**Architecture:** Move interfaces to Core, create separate services for parsing/templates/export, update FileIOController to use services via DI, move tests to ExcelIO.Tests.
|
|
|
|
**Tech Stack:** .NET 10, ClosedXML, xUnit, NSubstitute, Shouldly
|
|
|
|
---
|
|
|
|
## Phase 1: Rename Projects
|
|
|
|
### Task 1.1: Rename ExcelExport source project
|
|
|
|
**Step 1: Rename the project directory**
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && mv src/JdeScoping.ExcelExport src/JdeScoping.ExcelIO
|
|
```
|
|
|
|
**Step 2: Rename the .csproj file**
|
|
|
|
Run:
|
|
```bash
|
|
mv src/JdeScoping.ExcelIO/JdeScoping.ExcelExport.csproj src/JdeScoping.ExcelIO/JdeScoping.ExcelIO.csproj
|
|
```
|
|
|
|
**Step 3: Update namespace in all source files**
|
|
|
|
Run:
|
|
```bash
|
|
find src/JdeScoping.ExcelIO -name "*.cs" -type f ! -path "*/obj/*" ! -path "*/bin/*" -exec sed -i '' 's/JdeScoping\.ExcelExport/JdeScoping.ExcelIO/g' {} +
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.2: Rename ExcelExport test project
|
|
|
|
**Step 1: Rename the test project directory**
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && mv tests/JdeScoping.ExcelExport.Tests tests/JdeScoping.ExcelIO.Tests
|
|
```
|
|
|
|
**Step 2: Rename the .csproj file**
|
|
|
|
Run:
|
|
```bash
|
|
mv tests/JdeScoping.ExcelIO.Tests/JdeScoping.ExcelExport.Tests.csproj tests/JdeScoping.ExcelIO.Tests/JdeScoping.ExcelIO.Tests.csproj
|
|
```
|
|
|
|
**Step 3: Update project reference in test .csproj**
|
|
|
|
Modify `tests/JdeScoping.ExcelIO.Tests/JdeScoping.ExcelIO.Tests.csproj`:
|
|
Change:
|
|
```xml
|
|
<ProjectReference Include="..\..\src\JdeScoping.ExcelExport\JdeScoping.ExcelExport.csproj" />
|
|
```
|
|
To:
|
|
```xml
|
|
<ProjectReference Include="..\..\src\JdeScoping.ExcelIO\JdeScoping.ExcelIO.csproj" />
|
|
```
|
|
|
|
**Step 4: Update namespace in all test files**
|
|
|
|
Run:
|
|
```bash
|
|
find tests/JdeScoping.ExcelIO.Tests -name "*.cs" -type f ! -path "*/obj/*" ! -path "*/bin/*" -exec sed -i '' 's/JdeScoping\.ExcelExport/JdeScoping.ExcelIO/g' {} +
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.3: Update solution file
|
|
|
|
**Files:**
|
|
- Modify: `JdeScoping.slnx`
|
|
|
|
**Step 1: Update solution file paths**
|
|
|
|
In `JdeScoping.slnx`, change:
|
|
```xml
|
|
<Project Path="src/JdeScoping.ExcelExport/JdeScoping.ExcelExport.csproj" />
|
|
```
|
|
To:
|
|
```xml
|
|
<Project Path="src/JdeScoping.ExcelIO/JdeScoping.ExcelIO.csproj" />
|
|
```
|
|
|
|
And change:
|
|
```xml
|
|
<Project Path="tests/JdeScoping.ExcelExport.Tests/JdeScoping.ExcelExport.Tests.csproj" />
|
|
```
|
|
To:
|
|
```xml
|
|
<Project Path="tests/JdeScoping.ExcelIO.Tests/JdeScoping.ExcelIO.Tests.csproj" />
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.4: Update project references
|
|
|
|
**Step 1: Update Host project reference**
|
|
|
|
In `src/JdeScoping.Host/JdeScoping.Host.csproj`, change:
|
|
```xml
|
|
<ProjectReference Include="..\JdeScoping.ExcelExport\JdeScoping.ExcelExport.csproj" />
|
|
```
|
|
To:
|
|
```xml
|
|
<ProjectReference Include="..\JdeScoping.ExcelIO\JdeScoping.ExcelIO.csproj" />
|
|
```
|
|
|
|
**Step 2: Update Host Program.cs**
|
|
|
|
In `src/JdeScoping.Host/Program.cs`, change:
|
|
```csharp
|
|
using JdeScoping.ExcelExport;
|
|
```
|
|
To:
|
|
```csharp
|
|
using JdeScoping.ExcelIO;
|
|
```
|
|
|
|
And change:
|
|
```csharp
|
|
.AddExcelExport(builder.Configuration)
|
|
```
|
|
To:
|
|
```csharp
|
|
.AddExcelIO(builder.Configuration)
|
|
```
|
|
|
|
**Step 3: Update ServiceCollectionExtensions method name**
|
|
|
|
In `src/JdeScoping.ExcelIO/ServiceCollectionExtensions.cs`, change:
|
|
```csharp
|
|
public static IServiceCollection AddExcelExport(
|
|
```
|
|
To:
|
|
```csharp
|
|
public static IServiceCollection AddExcelIO(
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.5: Clean and verify build
|
|
|
|
**Step 1: Clean old build artifacts**
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && rm -rf src/JdeScoping.ExcelIO/obj src/JdeScoping.ExcelIO/bin tests/JdeScoping.ExcelIO.Tests/obj tests/JdeScoping.ExcelIO.Tests/bin
|
|
```
|
|
|
|
**Step 2: Restore and build**
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && dotnet restore && dotnet build
|
|
```
|
|
|
|
Expected: Build succeeded with 0 errors.
|
|
|
|
**Step 3: Run ExcelIO tests**
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && dotnet test tests/JdeScoping.ExcelIO.Tests --verbosity quiet
|
|
```
|
|
|
|
Expected: All tests pass.
|
|
|
|
---
|
|
|
|
## Phase 2: Add Interfaces to Core
|
|
|
|
### Task 2.1: Move IExcelExportService to Core
|
|
|
|
**Files:**
|
|
- Create: `src/JdeScoping.Core/Interfaces/IExcelExportService.cs`
|
|
- Delete: `src/JdeScoping.ExcelIO/Interfaces/IExcelExportService.cs`
|
|
|
|
**Step 1: Create interface in Core**
|
|
|
|
Create `src/JdeScoping.Core/Interfaces/IExcelExportService.cs`:
|
|
```csharp
|
|
namespace JdeScoping.Core.Interfaces;
|
|
|
|
/// <summary>
|
|
/// Service for generating Excel export files from search data.
|
|
/// </summary>
|
|
public interface IExcelExportService
|
|
{
|
|
/// <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>
|
|
Task<byte[]> GenerateAsync(object search, CancellationToken cancellationToken = default);
|
|
}
|
|
```
|
|
|
|
**Step 2: Delete old interface**
|
|
|
|
Run:
|
|
```bash
|
|
rm src/JdeScoping.ExcelIO/Interfaces/IExcelExportService.cs
|
|
```
|
|
|
|
**Step 3: Update ExcelExportService to use Core interface**
|
|
|
|
In `src/JdeScoping.ExcelIO/Export/ExcelExportService.cs` (after reorganization), update:
|
|
```csharp
|
|
using JdeScoping.Core.Interfaces;
|
|
```
|
|
|
|
Remove:
|
|
```csharp
|
|
using JdeScoping.ExcelIO.Interfaces;
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.2: Create IExcelTemplateService interface
|
|
|
|
**Files:**
|
|
- Create: `src/JdeScoping.Core/Interfaces/IExcelTemplateService.cs`
|
|
|
|
**Step 1: Create interface**
|
|
|
|
Create `src/JdeScoping.Core/Interfaces/IExcelTemplateService.cs`:
|
|
```csharp
|
|
namespace JdeScoping.Core.Interfaces;
|
|
|
|
/// <summary>
|
|
/// Service for generating Excel template files for data entry.
|
|
/// </summary>
|
|
public interface IExcelTemplateService
|
|
{
|
|
/// <summary>
|
|
/// Generates an Excel file with a single column of data.
|
|
/// </summary>
|
|
/// <typeparam name="T">Type of data items.</typeparam>
|
|
/// <param name="data">Data items to write.</param>
|
|
/// <param name="headerText">Header text for the column.</param>
|
|
/// <returns>Excel file as byte array.</returns>
|
|
byte[] GenerateSingleColumn<T>(IEnumerable<T> data, string headerText);
|
|
|
|
/// <summary>
|
|
/// Generates an Excel file with multiple columns of data.
|
|
/// </summary>
|
|
/// <param name="data">2D array of data (rows x columns).</param>
|
|
/// <param name="headers">Header text for each column.</param>
|
|
/// <returns>Excel file as byte array.</returns>
|
|
byte[] GenerateMultiColumn(object?[][] data, string[] headers);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.3: Create IExcelParserService interface
|
|
|
|
**Files:**
|
|
- Create: `src/JdeScoping.Core/Interfaces/IExcelParserService.cs`
|
|
|
|
**Step 1: Create interface**
|
|
|
|
Create `src/JdeScoping.Core/Interfaces/IExcelParserService.cs`:
|
|
```csharp
|
|
using JdeScoping.Core.ViewModels;
|
|
|
|
namespace JdeScoping.Core.Interfaces;
|
|
|
|
/// <summary>
|
|
/// Service for parsing Excel files uploaded by users.
|
|
/// </summary>
|
|
public interface IExcelParserService
|
|
{
|
|
/// <summary>
|
|
/// Parses work order numbers from an Excel file.
|
|
/// </summary>
|
|
/// <param name="fileStream">Excel file stream.</param>
|
|
/// <returns>List of work order numbers.</returns>
|
|
List<long> ParseWorkOrders(Stream fileStream);
|
|
|
|
/// <summary>
|
|
/// Parses item numbers from an Excel file.
|
|
/// </summary>
|
|
/// <param name="fileStream">Excel file stream.</param>
|
|
/// <returns>List of item numbers.</returns>
|
|
List<string> ParseItems(Stream fileStream);
|
|
|
|
/// <summary>
|
|
/// Parses component lot/item pairs from an Excel file.
|
|
/// </summary>
|
|
/// <param name="fileStream">Excel file stream.</param>
|
|
/// <returns>List of lot view models with LotNumber and ItemNumber.</returns>
|
|
List<LotViewModel> ParseComponentLots(Stream fileStream);
|
|
|
|
/// <summary>
|
|
/// Parses part operations from an Excel file.
|
|
/// </summary>
|
|
/// <param name="fileStream">Excel file stream.</param>
|
|
/// <returns>List of part operation view models.</returns>
|
|
List<PartOperationViewModel> ParsePartOperations(Stream fileStream);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: Create ExcelTemplateService
|
|
|
|
### Task 3.1: Create ExcelTemplateService
|
|
|
|
**Files:**
|
|
- Create: `src/JdeScoping.ExcelIO/Templates/ExcelTemplateService.cs`
|
|
|
|
**Step 1: Create Templates directory**
|
|
|
|
Run:
|
|
```bash
|
|
mkdir -p /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.ExcelIO/Templates
|
|
```
|
|
|
|
**Step 2: Create ExcelTemplateService**
|
|
|
|
Create `src/JdeScoping.ExcelIO/Templates/ExcelTemplateService.cs`:
|
|
```csharp
|
|
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();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3.2: Register ExcelTemplateService
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.ExcelIO/ServiceCollectionExtensions.cs`
|
|
|
|
**Step 1: Add registration**
|
|
|
|
In `src/JdeScoping.ExcelIO/ServiceCollectionExtensions.cs`, add the using statement:
|
|
```csharp
|
|
using JdeScoping.ExcelIO.Templates;
|
|
```
|
|
|
|
Add the registration after the existing services:
|
|
```csharp
|
|
// Register template service (singleton - stateless)
|
|
services.AddSingleton<IExcelTemplateService, ExcelTemplateService>();
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: Create ExcelParserService
|
|
|
|
### Task 4.1: Create ExcelParserService
|
|
|
|
**Files:**
|
|
- Create: `src/JdeScoping.ExcelIO/Parsing/ExcelParserService.cs`
|
|
|
|
**Step 1: Create Parsing directory**
|
|
|
|
Run:
|
|
```bash
|
|
mkdir -p /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.ExcelIO/Parsing
|
|
```
|
|
|
|
**Step 2: Create ExcelParserService**
|
|
|
|
Create `src/JdeScoping.ExcelIO/Parsing/ExcelParserService.cs`:
|
|
```csharp
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4.2: Register ExcelParserService
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.ExcelIO/ServiceCollectionExtensions.cs`
|
|
|
|
**Step 1: Add registration**
|
|
|
|
In `src/JdeScoping.ExcelIO/ServiceCollectionExtensions.cs`, add the using statement:
|
|
```csharp
|
|
using JdeScoping.ExcelIO.Parsing;
|
|
```
|
|
|
|
Add the registration:
|
|
```csharp
|
|
// Register parser service (singleton - stateless)
|
|
services.AddSingleton<IExcelParserService, ExcelParserService>();
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: Update FileIOController
|
|
|
|
### Task 5.1: Update FileController.cs (main partial class)
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Api/Controllers/FileController.cs`
|
|
|
|
**Step 1: Add service dependencies**
|
|
|
|
Replace entire file with:
|
|
```csharp
|
|
using JdeScoping.Core.Interfaces;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace JdeScoping.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Handles file upload/download operations for Excel templates.
|
|
/// </summary>
|
|
[Authorize]
|
|
[ApiController]
|
|
[Route("api/fileio")]
|
|
public partial class FileIOController : ApiControllerBase
|
|
{
|
|
private const string ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
|
|
private readonly ILotFinderRepository _repository;
|
|
private readonly IExcelParserService _parserService;
|
|
private readonly IExcelTemplateService _templateService;
|
|
private readonly ILogger<FileIOController> _logger;
|
|
|
|
public FileIOController(
|
|
ILotFinderRepository repository,
|
|
IExcelParserService parserService,
|
|
IExcelTemplateService templateService,
|
|
ILogger<FileIOController> logger)
|
|
{
|
|
_repository = repository;
|
|
_parserService = parserService;
|
|
_templateService = templateService;
|
|
_logger = logger;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5.2: Update FileIOController.WorkOrders.cs
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Api/Controllers/FileIOController.WorkOrders.cs`
|
|
|
|
**Step 1: Replace entire file**
|
|
|
|
```csharp
|
|
using JdeScoping.Api.Models;
|
|
using JdeScoping.Core.ViewModels;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace JdeScoping.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Work order file operations.
|
|
/// </summary>
|
|
public partial class FileIOController
|
|
{
|
|
/// <summary>
|
|
/// Uploads an Excel file containing work order numbers and returns the matched work orders
|
|
/// </summary>
|
|
[HttpPost("workorders/upload")]
|
|
[ProducesResponseType(typeof(FileUploadResult<WorkOrderViewModel>), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<FileUploadResult<WorkOrderViewModel>>> UploadWorkOrders(
|
|
IFormFile? file,
|
|
CancellationToken ct)
|
|
{
|
|
if (file is null)
|
|
{
|
|
return Ok(new FileUploadResult<WorkOrderViewModel>
|
|
{
|
|
WasSuccessful = false,
|
|
ErrorMessage = "No file uploaded"
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
using var stream = file.OpenReadStream();
|
|
var workOrderNumbers = _parserService.ParseWorkOrders(stream);
|
|
|
|
var workOrders = await _repository.LookupWorkordersAsync(workOrderNumbers, ct);
|
|
var viewModels = workOrders
|
|
.Select(wo => wo.ToViewModel())
|
|
.DistinctBy(wo => new { wo.WorkOrderNumber, wo.ItemNumber })
|
|
.OrderBy(wo => wo.WorkOrderNumber)
|
|
.ToArray();
|
|
|
|
return Ok(new FileUploadResult<WorkOrderViewModel>
|
|
{
|
|
WasSuccessful = true,
|
|
Data = viewModels
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to parse uploaded work order file");
|
|
return Ok(new FileUploadResult<WorkOrderViewModel>
|
|
{
|
|
WasSuccessful = false,
|
|
ErrorMessage = "Failed to parse uploaded file"
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads an Excel template with current work order data
|
|
/// </summary>
|
|
[HttpPost("workorders/download")]
|
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
|
public IActionResult DownloadWorkOrders([FromBody] List<long>? workOrders)
|
|
{
|
|
var data = _templateService.GenerateSingleColumn(workOrders ?? [], "Work Order Number");
|
|
return File(data, ContentType, "work_order_template.xlsx");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5.3: Update FileIOController.Items.cs
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Api/Controllers/FileIOController.Items.cs`
|
|
|
|
**Step 1: Replace entire file**
|
|
|
|
```csharp
|
|
using JdeScoping.Api.Models;
|
|
using JdeScoping.Core.ViewModels;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace JdeScoping.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Item file operations.
|
|
/// </summary>
|
|
public partial class FileIOController
|
|
{
|
|
/// <summary>
|
|
/// Uploads an Excel file containing item numbers and returns the matched items
|
|
/// </summary>
|
|
[HttpPost("items/upload")]
|
|
[ProducesResponseType(typeof(FileUploadResult<ItemViewModel>), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<FileUploadResult<ItemViewModel>>> UploadItems(
|
|
IFormFile? file,
|
|
CancellationToken ct)
|
|
{
|
|
if (file is null)
|
|
{
|
|
return Ok(new FileUploadResult<ItemViewModel>
|
|
{
|
|
WasSuccessful = false,
|
|
ErrorMessage = "No file uploaded"
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
using var stream = file.OpenReadStream();
|
|
var itemNumbers = _parserService.ParseItems(stream);
|
|
|
|
var items = await _repository.LookupItemsAsync(itemNumbers, ct);
|
|
var viewModels = items
|
|
.Select(i => i.ToViewModel())
|
|
.DistinctBy(i => new { i.ItemNumber, i.Description })
|
|
.ToArray();
|
|
|
|
return Ok(new FileUploadResult<ItemViewModel>
|
|
{
|
|
WasSuccessful = true,
|
|
Data = viewModels
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to parse uploaded items file");
|
|
return Ok(new FileUploadResult<ItemViewModel>
|
|
{
|
|
WasSuccessful = false,
|
|
ErrorMessage = "Failed to parse uploaded file"
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads an Excel template with current item data
|
|
/// </summary>
|
|
[HttpPost("items/download")]
|
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
|
public IActionResult DownloadItems([FromBody] List<ItemViewModel>? items)
|
|
{
|
|
var sourceData = (items ?? [])
|
|
.Select(i => new object?[] { i.ItemNumber })
|
|
.ToArray();
|
|
var headers = new[] { "Item Number" };
|
|
var data = _templateService.GenerateMultiColumn(sourceData, headers);
|
|
return File(data, ContentType, "item_number_template.xlsx");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5.4: Update FileIOController.ComponentLots.cs
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Api/Controllers/FileIOController.ComponentLots.cs`
|
|
|
|
**Step 1: Replace entire file**
|
|
|
|
```csharp
|
|
using JdeScoping.Api.Models;
|
|
using JdeScoping.Core.ViewModels;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace JdeScoping.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Component lot file operations.
|
|
/// </summary>
|
|
public partial class FileIOController
|
|
{
|
|
/// <summary>
|
|
/// Uploads an Excel file containing component lot/item pairs and returns the matched lots
|
|
/// </summary>
|
|
[HttpPost("componentlots/upload")]
|
|
[ProducesResponseType(typeof(FileUploadResult<LotViewModel>), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<FileUploadResult<LotViewModel>>> UploadComponentLots(
|
|
IFormFile? file,
|
|
CancellationToken ct)
|
|
{
|
|
if (file is null)
|
|
{
|
|
return Ok(new FileUploadResult<LotViewModel>
|
|
{
|
|
WasSuccessful = false,
|
|
ErrorMessage = "No file uploaded"
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
using var stream = file.OpenReadStream();
|
|
var lotViewModels = _parserService.ParseComponentLots(stream);
|
|
|
|
var lots = await _repository.LookupLotsAsync(lotViewModels, ct);
|
|
var viewModels = lots
|
|
.Select(l => l.ToViewModel())
|
|
.DistinctBy(l => new { l.LotNumber, l.ItemNumber })
|
|
.OrderBy(l => l.LotNumber)
|
|
.ToArray();
|
|
|
|
return Ok(new FileUploadResult<LotViewModel>
|
|
{
|
|
WasSuccessful = true,
|
|
Data = viewModels
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to parse uploaded component lots file");
|
|
return Ok(new FileUploadResult<LotViewModel>
|
|
{
|
|
WasSuccessful = false,
|
|
ErrorMessage = "Failed to parse uploaded file"
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads an Excel template with current component lot data
|
|
/// </summary>
|
|
[HttpPost("componentlots/download")]
|
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
|
public IActionResult DownloadComponentLots([FromBody] List<LotViewModel>? lotNumbers)
|
|
{
|
|
var sourceData = (lotNumbers ?? [])
|
|
.Select(l => new object?[] { l.LotNumber, l.ItemNumber })
|
|
.ToArray();
|
|
var headers = new[] { "Component Lot Number", "Component Item Number" };
|
|
var data = _templateService.GenerateMultiColumn(sourceData, headers);
|
|
return File(data, ContentType, "component_lot_template.xlsx");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5.5: Update FileIOController.PartOperations.cs
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.Api/Controllers/FileIOController.PartOperations.cs`
|
|
|
|
**Step 1: Replace entire file**
|
|
|
|
```csharp
|
|
using JdeScoping.Api.Models;
|
|
using JdeScoping.Core.ViewModels;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace JdeScoping.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Part operations file operations.
|
|
/// </summary>
|
|
public partial class FileIOController
|
|
{
|
|
/// <summary>
|
|
/// Uploads an Excel file containing part operations and returns the parsed data
|
|
/// </summary>
|
|
[HttpPost("partoperations/upload")]
|
|
[ProducesResponseType(typeof(FileUploadResult<PartOperationViewModel>), StatusCodes.Status200OK)]
|
|
public ActionResult<FileUploadResult<PartOperationViewModel>> UploadPartOperations(IFormFile? file)
|
|
{
|
|
if (file is null)
|
|
{
|
|
return Ok(new FileUploadResult<PartOperationViewModel>
|
|
{
|
|
WasSuccessful = false,
|
|
ErrorMessage = "No file uploaded"
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
using var stream = file.OpenReadStream();
|
|
var partOperations = _parserService.ParsePartOperations(stream);
|
|
|
|
return Ok(new FileUploadResult<PartOperationViewModel>
|
|
{
|
|
WasSuccessful = true,
|
|
Data = partOperations.ToArray()
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to parse uploaded part operations file");
|
|
return Ok(new FileUploadResult<PartOperationViewModel>
|
|
{
|
|
WasSuccessful = false,
|
|
ErrorMessage = "Failed to parse uploaded file"
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads an Excel template with current part operation data
|
|
/// </summary>
|
|
[HttpPost("partoperations/download")]
|
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
|
public IActionResult DownloadPartOperations([FromBody] List<PartOperationViewModel>? partOperations)
|
|
{
|
|
var sourceData = (partOperations ?? [])
|
|
.Select(po => new object?[] { po.ItemNumber, po.OperationNumber, po.MisNumber, po.MisRevision })
|
|
.ToArray();
|
|
var headers = new[] { "Item Number", "Operation Number", "MIS Number", "MIS Revision" };
|
|
var data = _templateService.GenerateMultiColumn(sourceData, headers);
|
|
return File(data, ContentType, "item_operations_mis_template.xlsx");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5.6: Delete old helper and update Api project
|
|
|
|
**Files:**
|
|
- Delete: `src/JdeScoping.Api/Helpers/ExcelTemplateGenerator.cs`
|
|
- Modify: `src/JdeScoping.Api/JdeScoping.Api.csproj`
|
|
|
|
**Step 1: Delete old helper**
|
|
|
|
Run:
|
|
```bash
|
|
rm /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.Api/Helpers/ExcelTemplateGenerator.cs
|
|
```
|
|
|
|
**Step 2: Update Api project to remove ClosedXML and add ExcelIO reference**
|
|
|
|
In `src/JdeScoping.Api/JdeScoping.Api.csproj`:
|
|
|
|
Remove this line:
|
|
```xml
|
|
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
|
```
|
|
|
|
Add project reference (if not already present):
|
|
```xml
|
|
<ProjectReference Include="..\JdeScoping.ExcelIO\JdeScoping.ExcelIO.csproj" />
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 6: Update Tests
|
|
|
|
### Task 6.1: Update FileControllerTests to use mocked services
|
|
|
|
**Files:**
|
|
- Modify: `tests/JdeScoping.Api.Tests/Controllers/FileControllerTests.cs`
|
|
|
|
**Step 1: Replace entire file**
|
|
|
|
```csharp
|
|
using JdeScoping.Api.Controllers;
|
|
using JdeScoping.Api.Models;
|
|
using JdeScoping.Core.Interfaces;
|
|
using JdeScoping.Core.Models.WorkOrders;
|
|
using JdeScoping.Core.ViewModels;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
using Shouldly;
|
|
|
|
namespace JdeScoping.Api.Tests.Controllers;
|
|
|
|
public class FileControllerTests
|
|
{
|
|
private readonly ILotFinderRepository _repository;
|
|
private readonly IExcelParserService _parserService;
|
|
private readonly IExcelTemplateService _templateService;
|
|
private readonly ILogger<FileIOController> _logger;
|
|
private readonly FileIOController _controller;
|
|
|
|
public FileControllerTests()
|
|
{
|
|
_repository = Substitute.For<ILotFinderRepository>();
|
|
_parserService = Substitute.For<IExcelParserService>();
|
|
_templateService = Substitute.For<IExcelTemplateService>();
|
|
_logger = Substitute.For<ILogger<FileIOController>>();
|
|
_controller = new FileIOController(_repository, _parserService, _templateService, _logger);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadWorkOrders_CallsParserAndRepository()
|
|
{
|
|
// Arrange
|
|
var formFile = CreateFormFile(new byte[] { 1, 2, 3 }, "workorders.xlsx");
|
|
var parsedNumbers = new List<long> { 12345, 67890 };
|
|
_parserService.ParseWorkOrders(Arg.Any<Stream>()).Returns(parsedNumbers);
|
|
|
|
var workOrders = new List<WorkOrder>
|
|
{
|
|
new() { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" },
|
|
new() { WorkOrderNumber = 67890, ItemNumber = "ITEM-002" }
|
|
};
|
|
_repository.LookupWorkordersAsync(parsedNumbers, Arg.Any<CancellationToken>())
|
|
.Returns(workOrders);
|
|
|
|
// Act
|
|
var result = await _controller.UploadWorkOrders(formFile, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Result.ShouldBeOfType<OkObjectResult>();
|
|
var okResult = (OkObjectResult)result.Result!;
|
|
var uploadResult = okResult.Value.ShouldBeOfType<FileUploadResult<WorkOrderViewModel>>();
|
|
uploadResult.WasSuccessful.ShouldBeTrue();
|
|
uploadResult.Data.ShouldNotBeNull();
|
|
uploadResult.Data.Length.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void DownloadWorkOrders_CallsTemplateService()
|
|
{
|
|
// Arrange
|
|
var workOrders = new List<long> { 12345, 67890 };
|
|
var expectedBytes = new byte[] { 1, 2, 3, 4, 5 };
|
|
_templateService.GenerateSingleColumn(workOrders, "Work Order Number").Returns(expectedBytes);
|
|
|
|
// Act
|
|
var result = _controller.DownloadWorkOrders(workOrders);
|
|
|
|
// Assert
|
|
result.ShouldBeOfType<FileContentResult>();
|
|
var fileResult = (FileContentResult)result;
|
|
fileResult.ContentType.ShouldBe("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
fileResult.FileDownloadName.ShouldBe("work_order_template.xlsx");
|
|
fileResult.FileContents.ShouldBe(expectedBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadWorkOrders_NoFile_ReturnsError()
|
|
{
|
|
// Act
|
|
var result = await _controller.UploadWorkOrders(null, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Result.ShouldBeOfType<OkObjectResult>();
|
|
var okResult = (OkObjectResult)result.Result!;
|
|
var uploadResult = okResult.Value.ShouldBeOfType<FileUploadResult<WorkOrderViewModel>>();
|
|
uploadResult.WasSuccessful.ShouldBeFalse();
|
|
uploadResult.ErrorMessage.ShouldBe("No file uploaded");
|
|
}
|
|
|
|
[Fact]
|
|
public void UploadPartOperations_CallsParser()
|
|
{
|
|
// Arrange
|
|
var formFile = CreateFormFile(new byte[] { 1, 2, 3 }, "partops.xlsx");
|
|
var parsedOps = new List<PartOperationViewModel>
|
|
{
|
|
new() { ItemNumber = "ITEM-001", OperationNumber = "100", MisNumber = "MIS001", MisRevision = "A" }
|
|
};
|
|
_parserService.ParsePartOperations(Arg.Any<Stream>()).Returns(parsedOps);
|
|
|
|
// Act
|
|
var result = _controller.UploadPartOperations(formFile);
|
|
|
|
// Assert
|
|
result.Result.ShouldBeOfType<OkObjectResult>();
|
|
var okResult = (OkObjectResult)result.Result!;
|
|
var uploadResult = okResult.Value.ShouldBeOfType<FileUploadResult<PartOperationViewModel>>();
|
|
uploadResult.WasSuccessful.ShouldBeTrue();
|
|
uploadResult.Data.ShouldNotBeNull();
|
|
uploadResult.Data.Length.ShouldBe(1);
|
|
}
|
|
|
|
private static IFormFile CreateFormFile(byte[] content, string fileName)
|
|
{
|
|
var stream = new MemoryStream(content);
|
|
var formFile = Substitute.For<IFormFile>();
|
|
formFile.OpenReadStream().Returns(stream);
|
|
formFile.FileName.Returns(fileName);
|
|
formFile.Length.Returns(content.Length);
|
|
return formFile;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6.2: Create ExcelParserServiceTests
|
|
|
|
**Files:**
|
|
- Create: `tests/JdeScoping.ExcelIO.Tests/Parsing/ExcelParserServiceTests.cs`
|
|
|
|
**Step 1: Create Parsing directory**
|
|
|
|
Run:
|
|
```bash
|
|
mkdir -p /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.ExcelIO.Tests/Parsing
|
|
```
|
|
|
|
**Step 2: Create test file**
|
|
|
|
Create `tests/JdeScoping.ExcelIO.Tests/Parsing/ExcelParserServiceTests.cs`:
|
|
```csharp
|
|
using ClosedXML.Excel;
|
|
using JdeScoping.ExcelIO.Parsing;
|
|
using Shouldly;
|
|
|
|
namespace JdeScoping.ExcelIO.Tests.Parsing;
|
|
|
|
public class ExcelParserServiceTests
|
|
{
|
|
private readonly ExcelParserService _service = new();
|
|
|
|
[Fact]
|
|
public void ParseWorkOrders_ReturnsWorkOrderNumbers()
|
|
{
|
|
// Arrange
|
|
var excelData = CreateWorkOrderExcel([12345, 67890, 11111]);
|
|
|
|
// Act
|
|
using var stream = new MemoryStream(excelData);
|
|
var result = _service.ParseWorkOrders(stream);
|
|
|
|
// Assert
|
|
result.Count.ShouldBe(3);
|
|
result.ShouldContain(12345);
|
|
result.ShouldContain(67890);
|
|
result.ShouldContain(11111);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseWorkOrders_SkipsInvalidNumbers()
|
|
{
|
|
// Arrange
|
|
using var workbook = new XLWorkbook();
|
|
var worksheet = workbook.Worksheets.Add("Sheet1");
|
|
worksheet.Cell(1, 1).Value = "Work Order";
|
|
worksheet.Cell(2, 1).Value = "12345";
|
|
worksheet.Cell(3, 1).Value = "not-a-number";
|
|
worksheet.Cell(4, 1).Value = "67890";
|
|
|
|
using var ms = new MemoryStream();
|
|
workbook.SaveAs(ms);
|
|
ms.Position = 0;
|
|
|
|
// Act
|
|
var result = _service.ParseWorkOrders(ms);
|
|
|
|
// Assert
|
|
result.Count.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseItems_ReturnsItemNumbers()
|
|
{
|
|
// Arrange
|
|
var excelData = CreateItemExcel(["ITEM-001", "ITEM-002"]);
|
|
|
|
// Act
|
|
using var stream = new MemoryStream(excelData);
|
|
var result = _service.ParseItems(stream);
|
|
|
|
// Assert
|
|
result.Count.ShouldBe(2);
|
|
result.ShouldContain("ITEM-001");
|
|
result.ShouldContain("ITEM-002");
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseComponentLots_ReturnsLotViewModels()
|
|
{
|
|
// Arrange
|
|
var excelData = CreateComponentLotExcel([("LOT001", "ITEM-001"), ("LOT002", "ITEM-002")]);
|
|
|
|
// Act
|
|
using var stream = new MemoryStream(excelData);
|
|
var result = _service.ParseComponentLots(stream);
|
|
|
|
// Assert
|
|
result.Count.ShouldBe(2);
|
|
result[0].LotNumber.ShouldBe("LOT001");
|
|
result[0].ItemNumber.ShouldBe("ITEM-001");
|
|
}
|
|
|
|
[Fact]
|
|
public void ParsePartOperations_ReturnsPartOperations()
|
|
{
|
|
// Arrange
|
|
var excelData = CreatePartOperationExcel([("ITEM-001", "100", "MIS001", "A")]);
|
|
|
|
// Act
|
|
using var stream = new MemoryStream(excelData);
|
|
var result = _service.ParsePartOperations(stream);
|
|
|
|
// Assert
|
|
result.Count.ShouldBe(1);
|
|
result[0].ItemNumber.ShouldBe("ITEM-001");
|
|
result[0].OperationNumber.ShouldBe("100");
|
|
result[0].MisNumber.ShouldBe("MIS001");
|
|
result[0].MisRevision.ShouldBe("A");
|
|
}
|
|
|
|
[Fact]
|
|
public void ParsePartOperations_TruncatesDecimalOperationNumbers()
|
|
{
|
|
// Arrange
|
|
var excelData = CreatePartOperationExcel([("ITEM-001", "100.5", "MIS001", "A")]);
|
|
|
|
// Act
|
|
using var stream = new MemoryStream(excelData);
|
|
var result = _service.ParsePartOperations(stream);
|
|
|
|
// Assert
|
|
result[0].OperationNumber.ShouldBe("100");
|
|
}
|
|
|
|
private static byte[] CreateWorkOrderExcel(long[] workOrderNumbers)
|
|
{
|
|
using var workbook = new XLWorkbook();
|
|
var worksheet = workbook.Worksheets.Add("Sheet1");
|
|
worksheet.Cell(1, 1).Value = "Work Order Number";
|
|
for (var i = 0; i < workOrderNumbers.Length; i++)
|
|
{
|
|
worksheet.Cell(i + 2, 1).Value = workOrderNumbers[i];
|
|
}
|
|
using var stream = new MemoryStream();
|
|
workbook.SaveAs(stream);
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static byte[] CreateItemExcel(string[] itemNumbers)
|
|
{
|
|
using var workbook = new XLWorkbook();
|
|
var worksheet = workbook.Worksheets.Add("Sheet1");
|
|
worksheet.Cell(1, 1).Value = "Item Number";
|
|
for (var i = 0; i < itemNumbers.Length; i++)
|
|
{
|
|
worksheet.Cell(i + 2, 1).Value = itemNumbers[i];
|
|
}
|
|
using var stream = new MemoryStream();
|
|
workbook.SaveAs(stream);
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static byte[] CreateComponentLotExcel((string LotNumber, string ItemNumber)[] lots)
|
|
{
|
|
using var workbook = new XLWorkbook();
|
|
var worksheet = workbook.Worksheets.Add("Sheet1");
|
|
worksheet.Cell(1, 1).Value = "Lot Number";
|
|
worksheet.Cell(1, 2).Value = "Item Number";
|
|
for (var i = 0; i < lots.Length; i++)
|
|
{
|
|
worksheet.Cell(i + 2, 1).Value = lots[i].LotNumber;
|
|
worksheet.Cell(i + 2, 2).Value = lots[i].ItemNumber;
|
|
}
|
|
using var stream = new MemoryStream();
|
|
workbook.SaveAs(stream);
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static byte[] CreatePartOperationExcel((string ItemNumber, string OpNumber, string MisNumber, string MisRevision)[] operations)
|
|
{
|
|
using var workbook = new XLWorkbook();
|
|
var worksheet = workbook.Worksheets.Add("Sheet1");
|
|
worksheet.Cell(1, 1).Value = "Item Number";
|
|
worksheet.Cell(1, 2).Value = "Operation Number";
|
|
worksheet.Cell(1, 3).Value = "MIS Number";
|
|
worksheet.Cell(1, 4).Value = "MIS Revision";
|
|
for (var i = 0; i < operations.Length; i++)
|
|
{
|
|
worksheet.Cell(i + 2, 1).Value = operations[i].ItemNumber;
|
|
worksheet.Cell(i + 2, 2).Value = operations[i].OpNumber;
|
|
worksheet.Cell(i + 2, 3).Value = operations[i].MisNumber;
|
|
worksheet.Cell(i + 2, 4).Value = operations[i].MisRevision;
|
|
}
|
|
using var stream = new MemoryStream();
|
|
workbook.SaveAs(stream);
|
|
return stream.ToArray();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6.3: Create ExcelTemplateServiceTests
|
|
|
|
**Files:**
|
|
- Create: `tests/JdeScoping.ExcelIO.Tests/Templates/ExcelTemplateServiceTests.cs`
|
|
|
|
**Step 1: Create Templates directory**
|
|
|
|
Run:
|
|
```bash
|
|
mkdir -p /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.ExcelIO.Tests/Templates
|
|
```
|
|
|
|
**Step 2: Create test file**
|
|
|
|
Create `tests/JdeScoping.ExcelIO.Tests/Templates/ExcelTemplateServiceTests.cs`:
|
|
```csharp
|
|
using ClosedXML.Excel;
|
|
using JdeScoping.ExcelIO.Templates;
|
|
using Shouldly;
|
|
|
|
namespace JdeScoping.ExcelIO.Tests.Templates;
|
|
|
|
public class ExcelTemplateServiceTests
|
|
{
|
|
private readonly ExcelTemplateService _service = new();
|
|
|
|
[Fact]
|
|
public void GenerateSingleColumn_CreatesValidExcel()
|
|
{
|
|
// Arrange
|
|
var data = new[] { 12345L, 67890L };
|
|
|
|
// Act
|
|
var result = _service.GenerateSingleColumn(data, "Work Order Number");
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.Length.ShouldBeGreaterThan(0);
|
|
|
|
// Verify content
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var worksheet = workbook.Worksheet(1);
|
|
|
|
worksheet.Cell(1, 1).GetString().ShouldBe("Work Order Number");
|
|
worksheet.Cell(2, 1).GetString().ShouldBe("12345");
|
|
worksheet.Cell(3, 1).GetString().ShouldBe("67890");
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateMultiColumn_CreatesValidExcel()
|
|
{
|
|
// Arrange
|
|
var data = new[]
|
|
{
|
|
new object?[] { "ITEM-001", "Description 1" },
|
|
new object?[] { "ITEM-002", "Description 2" }
|
|
};
|
|
var headers = new[] { "Item Number", "Description" };
|
|
|
|
// Act
|
|
var result = _service.GenerateMultiColumn(data, headers);
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.Length.ShouldBeGreaterThan(0);
|
|
|
|
// Verify content
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var worksheet = workbook.Worksheet(1);
|
|
|
|
worksheet.Cell(1, 1).GetString().ShouldBe("Item Number");
|
|
worksheet.Cell(1, 2).GetString().ShouldBe("Description");
|
|
worksheet.Cell(2, 1).GetString().ShouldBe("ITEM-001");
|
|
worksheet.Cell(2, 2).GetString().ShouldBe("Description 1");
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateSingleColumn_HandlesEmptyData()
|
|
{
|
|
// Act
|
|
var result = _service.GenerateSingleColumn(Array.Empty<string>(), "Header");
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.Length.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateMultiColumn_HandlesNullValues()
|
|
{
|
|
// Arrange
|
|
var data = new[]
|
|
{
|
|
new object?[] { "ITEM-001", null }
|
|
};
|
|
var headers = new[] { "Item", "Value" };
|
|
|
|
// Act
|
|
var result = _service.GenerateMultiColumn(data, headers);
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
using var stream = new MemoryStream(result);
|
|
using var workbook = new XLWorkbook(stream);
|
|
var worksheet = workbook.Worksheet(1);
|
|
|
|
worksheet.Cell(2, 2).GetString().ShouldBe(string.Empty);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 7: Final Updates
|
|
|
|
### Task 7.1: Update ExcelExportService to use Core interface
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.ExcelIO/ExcelExportService.cs`
|
|
|
|
**Step 1: Update using statements**
|
|
|
|
In `src/JdeScoping.ExcelIO/ExcelExportService.cs`, change:
|
|
```csharp
|
|
using JdeScoping.ExcelIO.Interfaces;
|
|
```
|
|
To:
|
|
```csharp
|
|
using JdeScoping.Core.Interfaces;
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7.2: Delete empty Interfaces folder
|
|
|
|
**Step 1: Remove empty folder**
|
|
|
|
Run:
|
|
```bash
|
|
rmdir /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.ExcelIO/Interfaces 2>/dev/null || rm -rf /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.ExcelIO/Interfaces
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7.3: Update ServiceCollectionExtensions completely
|
|
|
|
**Files:**
|
|
- Modify: `src/JdeScoping.ExcelIO/ServiceCollectionExtensions.cs`
|
|
|
|
**Step 1: Replace entire file**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.Interfaces;
|
|
using JdeScoping.ExcelIO.Configuration;
|
|
using JdeScoping.ExcelIO.Generators;
|
|
using JdeScoping.ExcelIO.Helpers;
|
|
using JdeScoping.ExcelIO.Parsing;
|
|
using JdeScoping.ExcelIO.Templates;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace JdeScoping.ExcelIO;
|
|
|
|
/// <summary>
|
|
/// Extension methods for registering Excel I/O services.
|
|
/// </summary>
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
/// <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;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 8: Verification
|
|
|
|
### Task 8.1: Build the solution
|
|
|
|
**Step 1: Clean and build**
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && dotnet clean && dotnet build
|
|
```
|
|
|
|
Expected: Build succeeded with 0 errors.
|
|
|
|
---
|
|
|
|
### Task 8.2: Run all tests
|
|
|
|
**Step 1: Execute test suite**
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && dotnet test --verbosity quiet
|
|
```
|
|
|
|
Expected: All tests pass.
|
|
|
|
---
|
|
|
|
### Task 8.3: Verify project structure
|
|
|
|
**Step 1: List source projects**
|
|
|
|
Run:
|
|
```bash
|
|
ls -1 /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/
|
|
```
|
|
|
|
Expected output should include `JdeScoping.ExcelIO` (not ExcelExport).
|
|
|
|
**Step 2: Verify no ClosedXML in Api project**
|
|
|
|
Run:
|
|
```bash
|
|
grep -l "ClosedXML" /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.Api/*.csproj /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.Api/**/*.cs 2>/dev/null || echo "No ClosedXML references found in Api - correct!"
|
|
```
|
|
|
|
Expected: "No ClosedXML references found in Api - correct!"
|
|
|
|
**Step 3: Verify interfaces in Core**
|
|
|
|
Run:
|
|
```bash
|
|
ls /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.Core/Interfaces/IExcel*.cs
|
|
```
|
|
|
|
Expected: Should list IExcelExportService.cs, IExcelTemplateService.cs, IExcelParserService.cs
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
**Total Tasks:** 22 tasks across 8 phases
|
|
|
|
- **Phase 1:** Rename projects (5 tasks)
|
|
- **Phase 2:** Add interfaces to Core (3 tasks)
|
|
- **Phase 3:** Create ExcelTemplateService (2 tasks)
|
|
- **Phase 4:** Create ExcelParserService (2 tasks)
|
|
- **Phase 5:** Update FileIOController (6 tasks)
|
|
- **Phase 6:** Update tests (3 tasks)
|
|
- **Phase 7:** Final updates (3 tasks)
|
|
- **Phase 8:** Verification (3 tasks)
|