feat: expand ExportsController with search and file endpoints
Add list/search, equipment/drawing number lookups, PDF hash tracking, cut template lookup, DXF zip download, and wire up FileStorageService and static files in Program.cs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
using FabWorks.Api.DTOs;
|
using FabWorks.Api.DTOs;
|
||||||
|
using FabWorks.Api.Services;
|
||||||
using FabWorks.Core.Data;
|
using FabWorks.Core.Data;
|
||||||
using FabWorks.Core.Models;
|
using FabWorks.Core.Models;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -11,8 +13,58 @@ namespace FabWorks.Api.Controllers
|
|||||||
public class ExportsController : ControllerBase
|
public class ExportsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly FabWorksDbContext _db;
|
private readonly FabWorksDbContext _db;
|
||||||
|
private readonly IFileStorageService _fileStorage;
|
||||||
|
|
||||||
public ExportsController(FabWorksDbContext db) => _db = db;
|
public ExportsController(FabWorksDbContext db, IFileStorageService fileStorage)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_fileStorage = fileStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<object>> List(
|
||||||
|
[FromQuery] string search = null,
|
||||||
|
[FromQuery] int skip = 0,
|
||||||
|
[FromQuery] int take = 50)
|
||||||
|
{
|
||||||
|
var query = _db.ExportRecords
|
||||||
|
.Include(r => r.BomItems)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
|
{
|
||||||
|
var term = search.Trim().ToLower();
|
||||||
|
query = query.Where(r =>
|
||||||
|
r.DrawingNumber.ToLower().Contains(term) ||
|
||||||
|
(r.Title != null && r.Title.ToLower().Contains(term)) ||
|
||||||
|
r.ExportedBy.ToLower().Contains(term) ||
|
||||||
|
r.BomItems.Any(b => b.PartName.ToLower().Contains(term) ||
|
||||||
|
b.Description.ToLower().Contains(term)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
|
||||||
|
var records = await query
|
||||||
|
.OrderByDescending(r => r.ExportedAt)
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(take)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
total,
|
||||||
|
items = records.Select(r => new
|
||||||
|
{
|
||||||
|
r.Id,
|
||||||
|
r.DrawingNumber,
|
||||||
|
r.Title,
|
||||||
|
r.SourceFilePath,
|
||||||
|
r.ExportedAt,
|
||||||
|
r.ExportedBy,
|
||||||
|
BomItemCount = r.BomItems?.Count ?? 0
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult<ExportDetailDto>> Create(CreateExportRequest request)
|
public async Task<ActionResult<ExportDetailDto>> Create(CreateExportRequest request)
|
||||||
@@ -20,6 +72,9 @@ namespace FabWorks.Api.Controllers
|
|||||||
var record = new ExportRecord
|
var record = new ExportRecord
|
||||||
{
|
{
|
||||||
DrawingNumber = request.DrawingNumber,
|
DrawingNumber = request.DrawingNumber,
|
||||||
|
Title = request.Title,
|
||||||
|
EquipmentNo = request.EquipmentNo,
|
||||||
|
DrawingNo = request.DrawingNo,
|
||||||
SourceFilePath = request.SourceFilePath,
|
SourceFilePath = request.SourceFilePath,
|
||||||
OutputFolder = request.OutputFolder,
|
OutputFolder = request.OutputFolder,
|
||||||
ExportedAt = DateTime.Now,
|
ExportedAt = DateTime.Now,
|
||||||
@@ -90,10 +145,163 @@ namespace FabWorks.Api.Controllers
|
|||||||
return (maxNum + 1).ToString();
|
return (maxNum + 1).ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("drawing-numbers")]
|
||||||
|
public async Task<ActionResult<List<string>>> GetDrawingNumbers()
|
||||||
|
{
|
||||||
|
var numbers = await _db.ExportRecords
|
||||||
|
.Select(r => r.DrawingNumber)
|
||||||
|
.Where(d => !string.IsNullOrEmpty(d))
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return numbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("equipment-numbers")]
|
||||||
|
public async Task<ActionResult<List<string>>> GetEquipmentNumbers()
|
||||||
|
{
|
||||||
|
var numbers = await _db.ExportRecords
|
||||||
|
.Select(r => r.EquipmentNo)
|
||||||
|
.Where(e => !string.IsNullOrEmpty(e))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(e => e)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return numbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("drawing-numbers-by-equipment")]
|
||||||
|
public async Task<ActionResult<List<string>>> GetDrawingNumbersByEquipment([FromQuery] string equipmentNo)
|
||||||
|
{
|
||||||
|
var query = _db.ExportRecords
|
||||||
|
.Where(r => !string.IsNullOrEmpty(r.DrawingNo));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(equipmentNo))
|
||||||
|
query = query.Where(r => r.EquipmentNo == equipmentNo);
|
||||||
|
|
||||||
|
var numbers = await query
|
||||||
|
.Select(r => r.DrawingNo)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(d => d)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return numbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("previous-pdf-hash")]
|
||||||
|
public async Task<ActionResult<string>> GetPreviousPdfHash(
|
||||||
|
[FromQuery] string drawingNumber,
|
||||||
|
[FromQuery] int? excludeId = null)
|
||||||
|
{
|
||||||
|
var hash = await _db.ExportRecords
|
||||||
|
.Where(r => r.DrawingNumber == drawingNumber
|
||||||
|
&& r.PdfContentHash != null
|
||||||
|
&& (excludeId == null || r.Id != excludeId))
|
||||||
|
.OrderByDescending(r => r.Id)
|
||||||
|
.Select(r => r.PdfContentHash)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (hash == null) return NotFound();
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}/pdf-hash")]
|
||||||
|
public async Task<IActionResult> UpdatePdfHash(int id, [FromBody] UpdatePdfHashRequest request)
|
||||||
|
{
|
||||||
|
var record = await _db.ExportRecords.FindAsync(id);
|
||||||
|
if (record == null) return NotFound();
|
||||||
|
|
||||||
|
record.PdfContentHash = request.PdfContentHash;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("previous-cut-template")]
|
||||||
|
public async Task<ActionResult<CutTemplateDto>> GetPreviousCutTemplate(
|
||||||
|
[FromQuery] string drawingNumber,
|
||||||
|
[FromQuery] string itemNo)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo))
|
||||||
|
return BadRequest("drawingNumber and itemNo are required.");
|
||||||
|
|
||||||
|
var ct = await _db.CutTemplates
|
||||||
|
.Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
|
||||||
|
&& c.BomItem.ItemNo == itemNo
|
||||||
|
&& c.ContentHash != null)
|
||||||
|
.OrderByDescending(c => c.Id)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (ct == null) return NotFound();
|
||||||
|
|
||||||
|
return new CutTemplateDto
|
||||||
|
{
|
||||||
|
Id = ct.Id,
|
||||||
|
DxfFilePath = ct.DxfFilePath,
|
||||||
|
ContentHash = ct.ContentHash,
|
||||||
|
Thickness = ct.Thickness,
|
||||||
|
KFactor = ct.KFactor,
|
||||||
|
DefaultBendRadius = ct.DefaultBendRadius
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/download-dxfs")]
|
||||||
|
public async Task<IActionResult> DownloadAllDxfs(int id)
|
||||||
|
{
|
||||||
|
var record = await _db.ExportRecords
|
||||||
|
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
|
|
||||||
|
if (record == null) return NotFound();
|
||||||
|
|
||||||
|
var dxfItems = record.BomItems
|
||||||
|
.Where(b => b.CutTemplate?.ContentHash != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (dxfItems.Count == 0) return NotFound("No DXF files for this export.");
|
||||||
|
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
||||||
|
{
|
||||||
|
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var b in dxfItems)
|
||||||
|
{
|
||||||
|
var ct = b.CutTemplate;
|
||||||
|
var fileName = ct.DxfFilePath?.Split(new[] { '/', '\\' }).LastOrDefault()
|
||||||
|
?? $"PT{(b.ItemNo ?? "").PadLeft(2, '0')}.dxf";
|
||||||
|
|
||||||
|
// Ensure unique names in zip
|
||||||
|
if (!usedNames.Add(fileName))
|
||||||
|
{
|
||||||
|
var baseName = Path.GetFileNameWithoutExtension(fileName);
|
||||||
|
var ext = Path.GetExtension(fileName);
|
||||||
|
var counter = 2;
|
||||||
|
do { fileName = $"{baseName}_{counter++}{ext}"; }
|
||||||
|
while (!usedNames.Add(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
var blobStream = _fileStorage.OpenBlob(ct.ContentHash, "dxf");
|
||||||
|
if (blobStream == null) continue;
|
||||||
|
|
||||||
|
var entry = zip.CreateEntry(fileName, CompressionLevel.Fastest);
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
await blobStream.CopyToAsync(entryStream);
|
||||||
|
blobStream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.Position = 0;
|
||||||
|
var zipName = $"{record.DrawingNumber ?? $"Export-{id}"} DXFs.zip";
|
||||||
|
return File(ms, "application/zip", zipName);
|
||||||
|
}
|
||||||
|
|
||||||
private static ExportDetailDto MapToDto(ExportRecord r) => new()
|
private static ExportDetailDto MapToDto(ExportRecord r) => new()
|
||||||
{
|
{
|
||||||
Id = r.Id,
|
Id = r.Id,
|
||||||
DrawingNumber = r.DrawingNumber,
|
DrawingNumber = r.DrawingNumber,
|
||||||
|
Title = r.Title,
|
||||||
|
EquipmentNo = r.EquipmentNo,
|
||||||
|
DrawingNo = r.DrawingNo,
|
||||||
SourceFilePath = r.SourceFilePath,
|
SourceFilePath = r.SourceFilePath,
|
||||||
OutputFolder = r.OutputFolder,
|
OutputFolder = r.OutputFolder,
|
||||||
ExportedAt = r.ExportedAt,
|
ExportedAt = r.ExportedAt,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using FabWorks.Api.Configuration;
|
||||||
using FabWorks.Api.Services;
|
using FabWorks.Api.Services;
|
||||||
using FabWorks.Core.Data;
|
using FabWorks.Core.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -9,7 +10,13 @@ builder.Services.AddDbContext<FabWorksDbContext>(options =>
|
|||||||
options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb")));
|
options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb")));
|
||||||
builder.Services.AddSingleton<FormProgramService>();
|
builder.Services.AddSingleton<FormProgramService>();
|
||||||
|
|
||||||
|
builder.Services.Configure<FileStorageOptions>(
|
||||||
|
builder.Configuration.GetSection(FileStorageOptions.SectionName));
|
||||||
|
builder.Services.AddScoped<IFileStorageService, FileStorageService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseDefaultFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
Reference in New Issue
Block a user