Files
jobtrackingapp/JobTrackerApi/Controllers/ExportController.cs
T
cesnimda 83e6430a24 feat: structured salary fields (min/max/currency/period)
Adds SalaryMin/SalaryMax/SalaryCurrency/SalaryPeriod alongside the
existing free-text Salary field (kept for back-compat and display).

- JobApplication model + idempotent column bridging for SQLite and MySQL
- Create/Update DTOs with NormalizeSalary (clamps negatives, swaps
  inverted min/max, uppercases currency, whitelists period)
- JobApplicationDto exposes the fields; CSV export gains 4 columns
- UI: add/edit dialogs get min/max/currency/period inputs; job table
  renders a formatted range via shared salary.ts formatter (falls back
  to free-text when structured values are absent)
- EN/NB translations; backend + full frontend suites green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:15:37 +02:00

103 lines
3.5 KiB
C#

using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using JobTrackerApi.Data;
namespace JobTrackerApi.Controllers
{
[ApiController]
[Route("api/export")]
[Authorize(AuthenticationSchemes = "local")]
public class ExportController : ControllerBase
{
private readonly JobTrackerContext _db;
public ExportController(JobTrackerContext db)
{
_db = db;
}
[HttpGet("jobs")]
public async Task<IActionResult> ExportJobs(
[FromQuery] string format = "json",
[FromQuery] bool includeDeleted = false,
CancellationToken cancellationToken = default
)
{
var query = _db.JobApplications
.AsNoTracking()
.Include(j => j.Company)
.AsQueryable();
if (!includeDeleted) query = query.Where(j => !j.IsDeleted);
var jobs = await query
.OrderByDescending(j => j.DateApplied)
.ToListAsync(cancellationToken);
var stamp = DateTime.Now.ToString("yyyy-MM-dd");
if (string.Equals(format, "csv", StringComparison.OrdinalIgnoreCase))
{
static string Esc(string? s)
{
s ??= "";
var needs = s.Contains(',') || s.Contains('"') || s.Contains('\n') || s.Contains('\r');
var q = s.Replace("\"", "\"\"");
return needs ? $"\"{q}\"" : q;
}
var sb = new StringBuilder();
sb.AppendLine(string.Join(",",
"Company",
"CompanyLocation",
"CompanySource",
"JobTitle",
"Status",
"DateApplied",
"Location",
"Salary",
"SalaryMin",
"SalaryMax",
"SalaryCurrency",
"SalaryPeriod",
"NextAction",
"FollowUpAt",
"JobUrl",
"Notes",
"CoverLetterText"
));
foreach (var j in jobs)
{
sb.AppendLine(string.Join(",",
Esc(j.Company?.Name),
Esc(j.Company?.Location),
Esc(j.Company?.Source),
Esc(j.JobTitle),
Esc(j.Status),
Esc(j.DateApplied.ToString("o")),
Esc(j.Location),
Esc(j.Salary),
Esc(j.SalaryMin?.ToString(System.Globalization.CultureInfo.InvariantCulture)),
Esc(j.SalaryMax?.ToString(System.Globalization.CultureInfo.InvariantCulture)),
Esc(j.SalaryCurrency),
Esc(j.SalaryPeriod),
Esc(j.NextAction),
Esc(j.FollowUpAt?.ToString("o")),
Esc(j.JobUrl),
Esc(j.Notes),
Esc(j.CoverLetterText)
));
}
return File(Encoding.UTF8.GetBytes(sb.ToString()), "text/csv", $"job-tracker-export-{stamp}.csv");
}
return File(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(jobs), "application/json", $"job-tracker-export-{stamp}.json");
}
}
}