diff --git a/FabWorks.Api/Controllers/ExportsController.cs b/FabWorks.Api/Controllers/ExportsController.cs index 743c857..1873aee 100644 --- a/FabWorks.Api/Controllers/ExportsController.cs +++ b/FabWorks.Api/Controllers/ExportsController.cs @@ -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> 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> 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>> 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>> 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>> 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> 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 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> 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 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(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, diff --git a/FabWorks.Api/Program.cs b/FabWorks.Api/Program.cs index dc2cc89..8121da5 100644 --- a/FabWorks.Api/Program.cs +++ b/FabWorks.Api/Program.cs @@ -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(options => options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb"))); builder.Services.AddSingleton(); +builder.Services.Configure( + builder.Configuration.GetSection(FileStorageOptions.SectionName)); +builder.Services.AddScoped(); + var app = builder.Build(); +app.UseDefaultFiles(); +app.UseStaticFiles(); app.MapControllers(); app.Run();