Files
jobtrackingapp/JobTrackerApi/Controllers/CorrespondenceController.cs

185 lines
7.8 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
namespace JobTrackerApi.Controllers
{
[ApiController]
[Route("api/correspondence")]
public class CorrespondenceController : ControllerBase
{
private readonly JobTrackerContext _db;
public CorrespondenceController(JobTrackerContext db)
{
_db = db;
}
// Resolve correspondence through its parent job so the DbContext's user-scoped
// job filter still protects raw id endpoints in multi-user deployments.
private Task<Correspondence?> FindOwnedMessageAsync(int correspondenceId, CancellationToken cancellationToken)
{
return _db.Correspondences
.Include(c => c.JobApplication)
.FirstOrDefaultAsync(c => c.Id == correspondenceId, cancellationToken);
}
public sealed record CorrespondenceInboxItemDto(
int Id,
int JobApplicationId,
string? CompanyName,
string? JobTitle,
string From,
string? Direction,
string? Subject,
string? Channel,
DateTime Date,
string ContentPreview,
string? ExternalThreadId,
string? ExternalFrom,
string? ExternalTo,
int LabelCount,
int AttachmentCount);
[HttpGet]
public async Task<ActionResult<List<CorrespondenceInboxItemDto>>> GetInbox(
[FromQuery] string? q,
[FromQuery] string? direction,
[FromQuery] string? linkState,
CancellationToken cancellationToken)
{
var query = _db.Correspondences
.Include(c => c.JobApplication)
.ThenInclude(j => j.Company)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(q))
{
var needle = q.Trim();
query = query.Where(c =>
(c.Subject != null && EF.Functions.Like(c.Subject, $"%{needle}%")) ||
EF.Functions.Like(c.Content, $"%{needle}%") ||
(c.ExternalFrom != null && EF.Functions.Like(c.ExternalFrom, $"%{needle}%")) ||
(c.JobApplication.JobTitle != null && EF.Functions.Like(c.JobApplication.JobTitle, $"%{needle}%")) ||
(c.JobApplication.Company.Name != null && EF.Functions.Like(c.JobApplication.Company.Name, $"%{needle}%")));
}
if (!string.IsNullOrWhiteSpace(direction) && !string.Equals(direction, "all", StringComparison.OrdinalIgnoreCase))
{
query = query.Where(c => c.Direction == direction);
}
if (string.Equals(linkState, "linked", StringComparison.OrdinalIgnoreCase))
{
query = query.Where(c => c.ExternalThreadId != null);
}
else if (string.Equals(linkState, "manual", StringComparison.OrdinalIgnoreCase))
{
query = query.Where(c => c.ExternalThreadId == null);
}
var items = await query
.OrderByDescending(c => c.Date)
.Take(200)
.Select(c => new CorrespondenceInboxItemDto(
c.Id,
c.JobApplicationId,
c.JobApplication.Company != null ? c.JobApplication.Company.Name : null,
c.JobApplication.JobTitle,
c.From,
c.Direction,
c.Subject,
c.Channel,
c.Date,
c.Content.Length <= 220 ? c.Content : c.Content.Substring(0, 220),
c.ExternalThreadId,
c.ExternalFrom,
c.ExternalTo,
c.ExternalLabelsJson != null ? 1 : 0,
c.AttachmentMetadataJson != null ? 1 : 0))
.ToListAsync(cancellationToken);
return Ok(items);
}
// GET all messages for a job
[HttpGet("{jobId:int}")]
public async Task<ActionResult<List<Correspondence>>> GetForJob([FromRoute] int jobId, CancellationToken cancellationToken)
{
var jobOk = await _db.JobApplications.AnyAsync(j => j.Id == jobId, cancellationToken);
if (!jobOk) return NotFound();
var messages = await _db.Correspondences
.Where(c => c.JobApplicationId == jobId)
.OrderBy(c => c.Date)
.ToListAsync(cancellationToken);
return Ok(messages);
}
public sealed record CreateCorrespondenceRequest(int JobApplicationId, string From, string Content);
public sealed record CreateCorrespondenceRequestV2(
int JobApplicationId,
string From,
string Content,
string? Subject,
string? Channel,
DateTime? Date,
string? Direction,
string? ExternalMessageId,
string? ExternalThreadId,
string? ExternalFrom,
string? ExternalTo,
string? ExternalLabelsJson,
string? AttachmentMetadataJson
);
// POST new message
[HttpPost]
public async Task<ActionResult<Correspondence>> Create([FromBody] CreateCorrespondenceRequestV2 request, CancellationToken cancellationToken)
{
if (request.JobApplicationId <= 0) return BadRequest("Valid jobApplicationId is required.");
if (string.IsNullOrWhiteSpace(request.From)) return BadRequest("From is required.");
if (string.IsNullOrWhiteSpace(request.Content)) return BadRequest("Content is required.");
var exists = await _db.JobApplications.AnyAsync(j => j.Id == request.JobApplicationId, cancellationToken);
if (!exists) return BadRequest("jobApplicationId does not exist.");
var message = new Correspondence
{
JobApplicationId = request.JobApplicationId,
From = request.From.Trim(),
Subject = string.IsNullOrWhiteSpace(request.Subject) ? null : request.Subject.Trim(),
Channel = string.IsNullOrWhiteSpace(request.Channel) ? null : request.Channel.Trim(),
Direction = string.IsNullOrWhiteSpace(request.Direction) ? null : request.Direction.Trim(),
ExternalMessageId = string.IsNullOrWhiteSpace(request.ExternalMessageId) ? null : request.ExternalMessageId.Trim(),
ExternalThreadId = string.IsNullOrWhiteSpace(request.ExternalThreadId) ? null : request.ExternalThreadId.Trim(),
ExternalFrom = string.IsNullOrWhiteSpace(request.ExternalFrom) ? null : request.ExternalFrom.Trim(),
ExternalTo = string.IsNullOrWhiteSpace(request.ExternalTo) ? null : request.ExternalTo.Trim(),
ExternalLabelsJson = string.IsNullOrWhiteSpace(request.ExternalLabelsJson) ? null : request.ExternalLabelsJson.Trim(),
AttachmentMetadataJson = string.IsNullOrWhiteSpace(request.AttachmentMetadataJson) ? null : request.AttachmentMetadataJson.Trim(),
Content = request.Content,
Date = request.Date ?? DateTime.Now,
};
_db.Correspondences.Add(message);
await _db.SaveChangesAsync(cancellationToken);
return CreatedAtAction(nameof(GetForJob), new { jobId = message.JobApplicationId }, message);
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete([FromRoute] int id, CancellationToken cancellationToken)
{
var message = await FindOwnedMessageAsync(id, cancellationToken);
if (message is null) return NotFound();
_db.Correspondences.Remove(message);
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}
}
}