Add OAth flow for Gmail and update tables and UI
This commit is contained in:
@@ -14,11 +14,13 @@ namespace JobTrackerApi.Controllers
|
||||
{
|
||||
private readonly JobTrackerContext _db;
|
||||
private readonly ISummarizerService _summarizer;
|
||||
private readonly IAppEmailSender _email;
|
||||
|
||||
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer)
|
||||
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, IAppEmailSender email)
|
||||
{
|
||||
_db = db;
|
||||
_summarizer = summarizer;
|
||||
_email = email;
|
||||
}
|
||||
|
||||
private string? CurrentUserId =>
|
||||
@@ -68,6 +70,30 @@ namespace JobTrackerApi.Controllers
|
||||
return new string(value.Trim().ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray());
|
||||
}
|
||||
|
||||
private static string? NormalizeTags(string? raw)
|
||||
{
|
||||
var normalized = SplitTags(raw)
|
||||
.Select(tag => tag.Trim())
|
||||
.Where(tag => tag.Length > 0)
|
||||
.GroupBy(tag => tag, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group =>
|
||||
{
|
||||
var first = group.First();
|
||||
return string.Join(" ", first.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(part => char.ToUpperInvariant(part[0]) + part[1..].ToLowerInvariant()));
|
||||
})
|
||||
.OrderBy(tag => tag, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return normalized.Count == 0 ? null : JsonSerializer.Serialize(normalized);
|
||||
}
|
||||
|
||||
private static string? NormalizeUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return null;
|
||||
var value = url.Trim();
|
||||
return Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri.ToString() : value;
|
||||
}
|
||||
|
||||
|
||||
public sealed record PagedResult<T>(List<T> Items, int Total, int Page, int PageSize);
|
||||
|
||||
@@ -538,10 +564,10 @@ namespace JobTrackerApi.Controllers
|
||||
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description,
|
||||
TranslatedDescription = string.IsNullOrWhiteSpace(request.TranslatedDescription) ? null : request.TranslatedDescription,
|
||||
DescriptionLanguage = string.IsNullOrWhiteSpace(request.DescriptionLanguage) ? null : request.DescriptionLanguage.Trim(),
|
||||
Tags = string.IsNullOrWhiteSpace(request.Tags) ? null : request.Tags,
|
||||
Tags = NormalizeTags(request.Tags),
|
||||
Deadline = request.Deadline,
|
||||
CoverLetterText = string.IsNullOrWhiteSpace(request.CoverLetterText) ? null : request.CoverLetterText,
|
||||
JobUrl = string.IsNullOrWhiteSpace(request.JobUrl) ? null : request.JobUrl,
|
||||
JobUrl = NormalizeUrl(request.JobUrl),
|
||||
DateApplied = request.DateApplied ?? DateTime.Now,
|
||||
ResponseReceived = false,
|
||||
ResponseDate = null,
|
||||
@@ -635,10 +661,10 @@ namespace JobTrackerApi.Controllers
|
||||
job.Description = request.Description;
|
||||
job.TranslatedDescription = request.TranslatedDescription;
|
||||
job.DescriptionLanguage = request.DescriptionLanguage;
|
||||
job.Tags = request.Tags;
|
||||
job.Tags = NormalizeTags(request.Tags);
|
||||
job.Deadline = request.Deadline;
|
||||
job.CoverLetterText = request.CoverLetterText;
|
||||
job.JobUrl = request.JobUrl;
|
||||
job.JobUrl = NormalizeUrl(request.JobUrl);
|
||||
if (request.DateApplied is not null) job.DateApplied = request.DateApplied.Value;
|
||||
|
||||
if (oldResponseReceived != job.ResponseReceived || oldResponseDate != job.ResponseDate)
|
||||
@@ -1076,6 +1102,7 @@ namespace JobTrackerApi.Controllers
|
||||
public sealed record DuplicateCandidateDto(int Id, string JobTitle, string Company, string? JobUrl, string Status, DateTime DateApplied, string Reason);
|
||||
public sealed record DuplicateCheckResult(bool HasDuplicates, List<DuplicateCandidateDto> Matches);
|
||||
public sealed record FollowUpDraftDto(string Subject, string Body, string Reason, DateTime SuggestedSendOn);
|
||||
public sealed record SendFollowUpRequest(string? ToEmail, string Subject, string Body, DateTime? NextFollowUpAt);
|
||||
public sealed record TagTrendResponse(List<string> Months, List<TagTrendSeries> Series);
|
||||
|
||||
[HttpGet("analytics-overview")]
|
||||
@@ -1310,6 +1337,50 @@ namespace JobTrackerApi.Controllers
|
||||
return Ok(new FollowUpDraftDto(subject, body, reason, DateTime.Today));
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/send-followup")]
|
||||
public async Task<IActionResult> SendFollowUp([FromRoute] int id, [FromBody] SendFollowUpRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var job = await _db.JobApplications
|
||||
.Include(j => j.Company)
|
||||
.FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
|
||||
|
||||
if (job is null) return NotFound();
|
||||
if (string.IsNullOrWhiteSpace(request.Subject)) return BadRequest("Subject is required.");
|
||||
if (string.IsNullOrWhiteSpace(request.Body)) return BadRequest("Body is required.");
|
||||
|
||||
var toEmail = (request.ToEmail ?? job.Company?.RecruiterEmail ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(toEmail)) return BadRequest("Recipient email is required.");
|
||||
|
||||
await _email.SendAsync(toEmail, request.Subject.Trim(), request.Body.Trim(), cancellationToken);
|
||||
|
||||
_db.Correspondences.Add(new Correspondence
|
||||
{
|
||||
JobApplicationId = id,
|
||||
From = "Me",
|
||||
Subject = request.Subject.Trim(),
|
||||
Channel = "Email",
|
||||
Content = request.Body.Trim(),
|
||||
Date = DateTime.Now,
|
||||
});
|
||||
|
||||
if (job.Company is not null)
|
||||
{
|
||||
job.Company.LastContactedAt = DateTime.Now;
|
||||
if (request.NextFollowUpAt is not null)
|
||||
{
|
||||
job.Company.NextContactAt = request.NextFollowUpAt.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (request.NextFollowUpAt is not null)
|
||||
{
|
||||
job.FollowUpAt = request.NextFollowUpAt.Value;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("summarizer-metrics")]
|
||||
public async Task<ActionResult<SummarizerMetrics>> GetSummarizerMetrics(CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user