185 lines
7.8 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|