feat(cv): rebuild professional resume studio

This commit is contained in:
cesnimda
2026-08-24 20:21:23 +02:00
parent dca5daa1a2
commit 367b70681a
27 changed files with 827 additions and 187 deletions
+39 -4
View File
@@ -4,7 +4,18 @@ using Microsoft.EntityFrameworkCore;
namespace JobTrackerApi.Services;
public sealed record CvVariantSummary(int Id, string Name, string ThemeId, string PublicSlug, bool IsPublic, int Version, int? JobApplicationId, DateTimeOffset UpdatedAtUtc);
public sealed record CvVariantSummary(
int Id,
string Name,
string ThemeId,
string Language,
string PublicSlug,
bool IsPublic,
int Version,
int? JobApplicationId,
DateTimeOffset UpdatedAtUtc,
string? JobTitle = null,
string? CompanyName = null);
public sealed record CvVariantVersionInfo(int Version, string Source, DateTimeOffset CreatedAtUtc, bool IsCurrent);
// Person + photo needed to render, resolved from ApplicationUser by the controller so this service
@@ -14,6 +25,7 @@ public sealed record CvRenderPerson(string FallbackName, string? PhotoDataUrl);
public interface ICvVariantService
{
Task<IReadOnlyList<CvVariantSummary>> ListAsync(string ownerUserId, CancellationToken ct);
Task<bool> CanAssociateJobAsync(string ownerUserId, int jobApplicationId, CancellationToken ct);
Task<CvVariant?> GetAsync(string ownerUserId, int id, CancellationToken ct);
Task<CvVariant> CreateAsync(string ownerUserId, string? name, int? jobApplicationId, CvVariantSettings? settings, CancellationToken ct);
Task<CvVariant?> SaveAsync(string ownerUserId, int id, string? name, CvVariantSettings settings, string source, CancellationToken ct);
@@ -49,18 +61,26 @@ public sealed class CvVariantService : ICvVariantService
public async Task<IReadOnlyList<CvVariantSummary>> ListAsync(string ownerUserId, CancellationToken ct)
{
var query = _db.CvVariants.Where(x => x.OwnerUserId == ownerUserId);
var query = _db.CvVariants
.Include(x => x.JobApplication)
.ThenInclude(x => x!.Company)
.Where(x => x.OwnerUserId == ownerUserId);
var variants = _db.Database.IsSqlite()
? (await query.ToListAsync(ct)).OrderByDescending(x => x.UpdatedAtUtc).ToList()
: await query.OrderByDescending(x => x.UpdatedAtUtc).ToListAsync(ct);
return variants.Select(Summarize).ToList();
}
public Task<bool> CanAssociateJobAsync(string ownerUserId, int jobApplicationId, CancellationToken ct) =>
_db.JobApplications.AnyAsync(job => job.Id == jobApplicationId && job.OwnerUserId == ownerUserId, ct);
public Task<CvVariant?> GetAsync(string ownerUserId, int id, CancellationToken ct) =>
_db.CvVariants.FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == ownerUserId, ct);
public async Task<CvVariant> CreateAsync(string ownerUserId, string? name, int? jobApplicationId, CvVariantSettings? settings, CancellationToken ct)
{
if (jobApplicationId is not null && !await CanAssociateJobAsync(ownerUserId, jobApplicationId.Value, ct))
throw new ArgumentException("The job application is unavailable.", nameof(jobApplicationId));
var now = DateTimeOffset.UtcNow;
var normalized = CvVariantSettingsJson.Normalize(settings);
var variant = new CvVariant
@@ -110,7 +130,11 @@ public sealed class CvVariantService : ICvVariantService
var source = await GetAsync(ownerUserId, id, ct);
if (source is null) return null;
var settings = CvVariantSettingsJson.Deserialize(source.SettingsJson);
return await CreateAsync(ownerUserId, string.IsNullOrWhiteSpace(newName) ? $"{source.Name} (copy)" : newName, source.JobApplicationId, settings, ct);
var associatedJobId = source.JobApplicationId is not null
&& await CanAssociateJobAsync(ownerUserId, source.JobApplicationId.Value, ct)
? source.JobApplicationId
: null;
return await CreateAsync(ownerUserId, string.IsNullOrWhiteSpace(newName) ? $"{source.Name} (copy)" : newName, associatedJobId, settings, ct);
}
public async Task<bool> DeleteAsync(string ownerUserId, int id, CancellationToken ct)
@@ -202,7 +226,18 @@ public sealed class CvVariantService : ICvVariantService
private static CvVariantSummary Summarize(CvVariant v)
{
var settings = CvVariantSettingsJson.Deserialize(v.SettingsJson);
return new CvVariantSummary(v.Id, v.Name, settings.ThemeId, v.PublicSlug, v.IsPublic, v.Version, v.JobApplicationId, v.UpdatedAtUtc);
return new CvVariantSummary(
v.Id,
v.Name,
settings.ThemeId,
settings.Language ?? "en",
v.PublicSlug,
v.IsPublic,
v.Version,
v.JobApplicationId,
v.UpdatedAtUtc,
v.JobApplication?.JobTitle,
v.JobApplication?.Company?.Name);
}
private static string CleanName(string? name) => string.IsNullOrWhiteSpace(name) ? "Untitled CV" : name.Trim();