using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using static JobTrackerApi.Services.JobApplicationHelpers; namespace JobTrackerApi.Controllers; [ApiController] [Route("api/jobapplications")] [Authorize(AuthenticationSchemes = "local")] public sealed class JobApplicationAnalyticsController : ControllerBase { private readonly JobTrackerContext _db; private readonly AnalyticsService _analytics; public JobApplicationAnalyticsController(JobTrackerContext db, AnalyticsService analytics) { _db = db; _analytics = analytics; } [HttpGet("stats")] public async Task> GetStats(CancellationToken cancellationToken) => Ok(await _analytics.GetStatsAsync(cancellationToken)); [HttpGet("analytics")] public async Task>> GetAnalytics( [FromQuery] int months = 12, [FromQuery] DateTime? from = null, [FromQuery] DateTime? to = null, CancellationToken cancellationToken = default) { months = Math.Clamp(months, 3, 36); var now = DateTime.Now; DateTime startMonth; DateTime endMonth; if (from is not null || to is not null) { var toValue = to ?? now; var fromValue = from ?? toValue.AddMonths(-months); if (toValue < fromValue) (fromValue, toValue) = (toValue, fromValue); startMonth = new DateTime(fromValue.Year, fromValue.Month, 1); endMonth = new DateTime(toValue.Year, toValue.Month, 1).AddMonths(1); var spanMonths = ((endMonth.Year - startMonth.Year) * 12) + endMonth.Month - startMonth.Month; spanMonths = Math.Clamp(spanMonths, 3, 36); startMonth = endMonth.AddMonths(-spanMonths); months = spanMonths; } else { endMonth = new DateTime(now.Year, now.Month, 1).AddMonths(1); startMonth = endMonth.AddMonths(-months); } var jobs = await _db.JobApplications .AsNoTracking() .Where(job => !job.IsDeleted && job.DateApplied != null && job.DateApplied >= startMonth && job.DateApplied < endMonth) .Select(job => new { job.DateApplied, job.ResponseDate }) .ToListAsync(cancellationToken); var applied = new Dictionary(StringComparer.Ordinal); var responses = new Dictionary(StringComparer.Ordinal); static string Key(DateTime value) => $"{value:yyyy-MM}"; foreach (var job in jobs) { var appliedKey = Key(job.DateApplied!.Value); applied[appliedKey] = (applied.TryGetValue(appliedKey, out var appliedCount) ? appliedCount : 0) + 1; if (job.ResponseDate is not null) { var responseKey = Key(job.ResponseDate.Value); responses[responseKey] = (responses.TryGetValue(responseKey, out var responseCount) ? responseCount : 0) + 1; } } var result = new List(months); for (var index = 0; index < months; index++) { var key = Key(startMonth.AddMonths(index)); applied.TryGetValue(key, out var appliedCount); responses.TryGetValue(key, out var responseCount); result.Add(new AnalyticsPoint(key, appliedCount, responseCount)); } return Ok(result); } [HttpGet("tags")] public async Task>> GetTags( [FromQuery] int limit = 10, [FromQuery] DateTime? from = null, [FromQuery] DateTime? to = null, CancellationToken cancellationToken = default) { limit = Math.Clamp(limit, 3, 50); IQueryable query = _db.JobApplications.AsNoTracking().Where(job => !job.IsDeleted); if (from is not null || to is not null) { var toValue = to ?? DateTime.Now; var fromValue = from ?? DateTime.MinValue; if (toValue < fromValue) (fromValue, toValue) = (toValue, fromValue); var startMonth = fromValue == DateTime.MinValue ? (DateTime?)null : new DateTime(fromValue.Year, fromValue.Month, 1); var endMonth = new DateTime(toValue.Year, toValue.Month, 1).AddMonths(1); if (startMonth is not null) query = query.Where(job => job.DateApplied >= startMonth.Value); query = query.Where(job => job.DateApplied < endMonth); } var tagStrings = await query.Select(job => job.Tags).ToListAsync(cancellationToken); var counts = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var tagString in tagStrings) { foreach (var tag in SplitTags(tagString)) { counts[tag] = counts.TryGetValue(tag, out var value) ? (value.Display, value.Count + 1) : (tag, 1); } } return Ok(counts.Values .OrderByDescending(value => value.Count) .ThenBy(value => value.Display, StringComparer.OrdinalIgnoreCase) .Take(limit) .Select(value => new TagPoint(value.Display, value.Count)) .ToList()); } [HttpGet("analytics-overview")] public async Task> GetAnalyticsOverview(CancellationToken cancellationToken) => Ok(await _analytics.GetAnalyticsOverviewAsync(cancellationToken)); [HttpGet("tag-trends")] public async Task> GetTagTrends( [FromQuery] int months = 6, [FromQuery] int limit = 5, CancellationToken cancellationToken = default) { months = Math.Clamp(months, 3, 24); limit = Math.Clamp(limit, 3, 10); var endMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1).AddMonths(1); var startMonth = endMonth.AddMonths(-months); var jobs = await _db.JobApplications .AsNoTracking() .Where(job => !job.IsDeleted && job.DateApplied >= startMonth && job.DateApplied < endMonth) .Select(job => new { job.DateApplied, job.Tags }) .ToListAsync(cancellationToken); var totals = new Dictionary(StringComparer.OrdinalIgnoreCase); var monthKeys = Enumerable.Range(0, months).Select(index => startMonth.AddMonths(index).ToString("yyyy-MM")).ToList(); var countsByTag = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var job in jobs) { var monthKey = $"{job.DateApplied:yyyy-MM}"; foreach (var tag in SplitTags(job.Tags)) { totals[tag] = (totals.TryGetValue(tag, out var total) ? total : 0) + 1; if (!countsByTag.TryGetValue(tag, out var countsByMonth)) { countsByMonth = new Dictionary(StringComparer.Ordinal); countsByTag[tag] = countsByMonth; } countsByMonth[monthKey] = (countsByMonth.TryGetValue(monthKey, out var count) ? count : 0) + 1; } } var topTags = totals .OrderByDescending(value => value.Value) .ThenBy(value => value.Key, StringComparer.OrdinalIgnoreCase) .Take(limit) .Select(value => value.Key) .ToList(); var series = topTags .Select(tag => new TagTrendSeries( tag, monthKeys.Select(month => countsByTag.TryGetValue(tag, out var byMonth) && byMonth.TryGetValue(month, out var count) ? count : 0).ToList())) .ToList(); return Ok(new TagTrendResponse(monthKeys, series)); } }