eac34705e3
Unblocks the documented core workflow and closes the AI-service exposure, without changing existing behaviour. Job/JobApplication split (additive; see ADR-002): - New Job entity (the opportunity) with owner-scoped query filter; nullable JobApplication.JobId FK. Nothing reads Job yet. - Migration AddJobEntityAndProspectStages, hand-edited to drop reconciler-owned tables the scaffolder re-emitted; verified against the real dev DB. Pipeline: 10 internal stages across three concerns kept separate — PipelineStage (workflow) / PipelineGroup (UI: NotApplied/Active/Closed) / PipelineCategory (analytics). Adds Saved/Interested/Preparing/Withdrawn; keeps Waiting and Ghosted. Kanban shows 3 grouped columns; cards keep a stage chip and full transitions; drag applies only safe transitions (never infers Ghosted/Withdrawn). DateApplied nullable + SavedAt. Cleared when leaving Applied so analytics stay accurate; the discarded date is preserved as an AppliedDateCleared JobEvent. AI service lockdown: no host port; private ai_internal network (backend is the only other member); X-Ai-Service-Token required on all non-/health endpoints; AI_SERVICE_TOKEN mandatory via compose. Verified backend-only against the live stack. Also carries two pre-existing working-tree files (views/ProfilePage.tsx, views/CareerWorkspacePage.tsx) so the tree is clean for the branch integration. Tests: +40 backend (247 total), +5 sidecar (16), +15 frontend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
3.5 KiB
C#
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");
|
|
}
|
|
}
|
|
}
|
|
|