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.Services;
|
||||
using FabWorks.Core.Data;
|
||||
using FabWorks.Core.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -11,8 +13,58 @@ namespace FabWorks.Api.Controllers
|
||||
public class ExportsController : ControllerBase
|
||||
{
|
||||
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]
|
||||
public async Task<ActionResult<ExportDetailDto>> Create(CreateExportRequest request)
|
||||
@@ -20,6 +72,9 @@ namespace FabWorks.Api.Controllers
|
||||
var record = new ExportRecord
|
||||
{
|
||||
DrawingNumber = request.DrawingNumber,
|
||||
Title = request.Title,
|
||||
EquipmentNo = request.EquipmentNo,
|
||||
DrawingNo = request.DrawingNo,
|
||||
SourceFilePath = request.SourceFilePath,
|
||||
OutputFolder = request.OutputFolder,
|
||||
ExportedAt = DateTime.Now,
|
||||
@@ -90,10 +145,163 @@ namespace FabWorks.Api.Controllers
|
||||
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()
|
||||
{
|
||||
Id = r.Id,
|
||||
DrawingNumber = r.DrawingNumber,
|
||||
Title = r.Title,
|
||||
EquipmentNo = r.EquipmentNo,
|
||||
DrawingNo = r.DrawingNo,
|
||||
SourceFilePath = r.SourceFilePath,
|
||||
OutputFolder = r.OutputFolder,
|
||||
ExportedAt = r.ExportedAt,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using FabWorks.Api.Configuration;
|
||||
using FabWorks.Api.Services;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -9,7 +10,13 @@ builder.Services.AddDbContext<FabWorksDbContext>(options =>
|
||||
options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb")));
|
||||
builder.Services.AddSingleton<FormProgramService>();
|
||||
|
||||
builder.Services.Configure<FileStorageOptions>(
|
||||
builder.Configuration.GetSection(FileStorageOptions.SectionName));
|
||||
builder.Services.AddScoped<IFileStorageService, FileStorageService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
|
||||
Reference in New Issue
Block a user