Add attachment metadata and overview strategy snapshot
This commit is contained in:
@@ -27,7 +27,7 @@ namespace JobTrackerApi.Controllers
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public sealed record AttachmentDto(int Id, string FileName, DateTime UploadDate, string FileType, long FileSize);
|
||||
public sealed record AttachmentDto(int Id, string FileName, DateTime UploadDate, string FileType, long FileSize, string? Purpose, bool UseForAi);
|
||||
|
||||
// Child entities are accessed by raw integer ids in a few endpoints below.
|
||||
// Always resolve them through the parent JobApplication query so the global job-level
|
||||
@@ -46,6 +46,17 @@ namespace JobTrackerApi.Controllers
|
||||
return $"{DateTime.UtcNow:yyyyMMddHHmmssfff}-{suffix}{ext}";
|
||||
}
|
||||
|
||||
private static string GuessPurpose(string fileName)
|
||||
{
|
||||
var n = (fileName ?? string.Empty).ToLowerInvariant();
|
||||
if (n.Contains("cover")) return "cover-letter";
|
||||
if (n.Contains("resume") || n.Contains("résumé") || n.Contains(" cv") || n.EndsWith("cv.pdf")) return "resume";
|
||||
if (n.Contains("portfolio")) return "portfolio";
|
||||
if (n.Contains("case") || n.Contains("sample")) return "case-study";
|
||||
if (n.Contains("cert")) return "certificate";
|
||||
return "other";
|
||||
}
|
||||
|
||||
[HttpGet("{jobId:int}")]
|
||||
public async Task<ActionResult<List<AttachmentDto>>> ListForJob([FromRoute] int jobId, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -56,7 +67,7 @@ namespace JobTrackerApi.Controllers
|
||||
.AsNoTracking()
|
||||
.Where(a => a.JobApplicationId == jobId)
|
||||
.OrderByDescending(a => a.UploadDate)
|
||||
.Select(a => new AttachmentDto(a.Id, a.FileName, a.UploadDate, a.FileType, a.FileSize))
|
||||
.Select(a => new AttachmentDto(a.Id, a.FileName, a.UploadDate, a.FileType, a.FileSize, a.Purpose, a.UseForAi))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(items);
|
||||
@@ -76,17 +87,32 @@ namespace JobTrackerApi.Controllers
|
||||
return PhysicalFile(att.FilePath, contentType, fileName);
|
||||
}
|
||||
|
||||
public sealed record RenameAttachmentRequest(string FileName);
|
||||
public sealed record UpdateAttachmentRequest(string? FileName, string? Purpose, bool? UseForAi);
|
||||
|
||||
[HttpPatch("{id:int}")]
|
||||
public async Task<IActionResult> Rename([FromRoute] int id, [FromBody] RenameAttachmentRequest request, CancellationToken cancellationToken)
|
||||
public async Task<IActionResult> Rename([FromRoute] int id, [FromBody] UpdateAttachmentRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var att = await FindOwnedAttachmentAsync(id, cancellationToken);
|
||||
if (att is null) return NotFound();
|
||||
|
||||
var name = Path.GetFileName((request.FileName ?? "").Trim());
|
||||
if (name.Length == 0) return BadRequest("FileName is required.");
|
||||
if (request.UseForAi is not null)
|
||||
{
|
||||
att.UseForAi = request.UseForAi.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Purpose))
|
||||
{
|
||||
att.Purpose = request.Purpose.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
var rawName = (request.FileName ?? string.Empty).Trim();
|
||||
if (rawName.Length == 0)
|
||||
{
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
var name = Path.GetFileName(rawName);
|
||||
var ext = Path.GetExtension(name);
|
||||
if (!AllowedExtensions.Contains(ext))
|
||||
return BadRequest("That file type is not allowed.");
|
||||
@@ -166,7 +192,9 @@ namespace JobTrackerApi.Controllers
|
||||
FilePath = path,
|
||||
UploadDate = DateTime.Now,
|
||||
FileType = string.IsNullOrWhiteSpace(file.ContentType) ? "application/octet-stream" : file.ContentType,
|
||||
FileSize = file.Length
|
||||
FileSize = file.Length,
|
||||
Purpose = GuessPurpose(displayName),
|
||||
UseForAi = true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user