255 lines
11 KiB
C#
255 lines
11 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace JobTrackerApi.Models;
|
|
|
|
public static class TailoredCvDraftJson
|
|
{
|
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
};
|
|
|
|
public static TailoredCvDocument Empty() => Normalize(new TailoredCvDocument());
|
|
|
|
public static TailoredCvDocument FromDraft(TailoredCvDraft? draft)
|
|
{
|
|
if (draft is null) return Empty();
|
|
|
|
var document = new TailoredCvDocument
|
|
{
|
|
TemplateId = string.IsNullOrWhiteSpace(draft.TemplateId) ? "base" : draft.TemplateId.Trim(),
|
|
Headline = TrimOrNull(draft.Headline),
|
|
Summary = DeserializeList(draft.SummaryJson),
|
|
SelectedSkills = DeserializeList(draft.SelectedSkillsJson),
|
|
Experience = DeserializeList<TailoredCvExperienceItem>(draft.ExperienceJson)
|
|
.Select(NormalizeExperience)
|
|
.Where(item => !string.IsNullOrWhiteSpace(item.Title) || !string.IsNullOrWhiteSpace(item.Company) || item.Bullets.Count > 0)
|
|
.ToList(),
|
|
Education = DeserializeList<TailoredCvEducationItem>(draft.EducationJson)
|
|
.Select(NormalizeEducation)
|
|
.Where(item => !string.IsNullOrWhiteSpace(item.Qualification) || !string.IsNullOrWhiteSpace(item.Institution) || item.Details.Count > 0)
|
|
.ToList(),
|
|
CustomSections = DeserializeList<TailoredCvCustomSection>(draft.CustomSectionsJson)
|
|
.Select(NormalizeCustomSection)
|
|
.Where(item => !string.IsNullOrWhiteSpace(item.Title) || item.Items.Count > 0)
|
|
.ToList(),
|
|
RenderOptions = DeserializeObject<TailoredCvRenderOptions>(draft.RenderOptionsJson) ?? new TailoredCvRenderOptions(),
|
|
};
|
|
|
|
return Normalize(document);
|
|
}
|
|
|
|
public static void ApplyToDraft(TailoredCvDraft draft, TailoredCvDocument? document)
|
|
{
|
|
var normalized = Normalize(document);
|
|
draft.TemplateId = normalized.TemplateId;
|
|
draft.Headline = TrimOrNull(normalized.Headline);
|
|
draft.SummaryJson = JsonSerializer.Serialize(normalized.Summary, SerializerOptions);
|
|
draft.SelectedSkillsJson = JsonSerializer.Serialize(normalized.SelectedSkills, SerializerOptions);
|
|
draft.ExperienceJson = JsonSerializer.Serialize(normalized.Experience, SerializerOptions);
|
|
draft.EducationJson = JsonSerializer.Serialize(normalized.Education, SerializerOptions);
|
|
draft.CustomSectionsJson = JsonSerializer.Serialize(normalized.CustomSections, SerializerOptions);
|
|
draft.RenderOptionsJson = JsonSerializer.Serialize(normalized.RenderOptions, SerializerOptions);
|
|
}
|
|
|
|
public static TailoredCvDocument Normalize(TailoredCvDocument? document)
|
|
{
|
|
document ??= new TailoredCvDocument();
|
|
document.TemplateId = string.IsNullOrWhiteSpace(document.TemplateId) ? "ats-minimal" : document.TemplateId.Trim();
|
|
document.Headline = TrimOrNull(document.Headline);
|
|
document.Summary = CleanList(document.Summary);
|
|
document.SelectedSkills = CleanList(document.SelectedSkills);
|
|
document.Experience = (document.Experience ?? new List<TailoredCvExperienceItem>())
|
|
.Select(NormalizeExperience)
|
|
.Where(item => !string.IsNullOrWhiteSpace(item.Title) || !string.IsNullOrWhiteSpace(item.Company) || item.Bullets.Count > 0)
|
|
.ToList();
|
|
document.Education = (document.Education ?? new List<TailoredCvEducationItem>())
|
|
.Select(NormalizeEducation)
|
|
.Where(item => !string.IsNullOrWhiteSpace(item.Qualification) || !string.IsNullOrWhiteSpace(item.Institution) || item.Details.Count > 0)
|
|
.ToList();
|
|
document.CustomSections = (document.CustomSections ?? new List<TailoredCvCustomSection>())
|
|
.Select(NormalizeCustomSection)
|
|
.Where(item => !string.IsNullOrWhiteSpace(item.Title) || item.Items.Count > 0)
|
|
.ToList();
|
|
document.RenderOptions ??= new TailoredCvRenderOptions();
|
|
document.RenderOptions.PageMode = string.IsNullOrWhiteSpace(document.RenderOptions.PageMode) ? "one-page" : document.RenderOptions.PageMode.Trim();
|
|
document.RenderOptions.AccentColor = string.IsNullOrWhiteSpace(document.RenderOptions.AccentColor) ? "slate" : document.RenderOptions.AccentColor.Trim();
|
|
document.RenderOptions.BulletDensity = string.IsNullOrWhiteSpace(document.RenderOptions.BulletDensity) ? "balanced" : document.RenderOptions.BulletDensity.Trim();
|
|
document.RenderOptions.SectionOrder = CleanList(document.RenderOptions.SectionOrder);
|
|
if (document.RenderOptions.SectionOrder.Count == 0)
|
|
{
|
|
document.RenderOptions.SectionOrder = new List<string> { "summary", "skills", "experience", "education", "custom" };
|
|
}
|
|
return document;
|
|
}
|
|
|
|
public static string RenderPlainText(TailoredCvDocument? document)
|
|
{
|
|
var normalized = Normalize(document);
|
|
var lines = new List<string>();
|
|
|
|
AddLine(lines, normalized.Headline);
|
|
if (normalized.Summary.Count > 0)
|
|
{
|
|
AddBlock(lines, "Professional Summary", normalized.Summary.Select(item => $"- {item}"));
|
|
}
|
|
|
|
if (normalized.SelectedSkills.Count > 0)
|
|
{
|
|
AddBlock(lines, "Core Skills", normalized.SelectedSkills);
|
|
}
|
|
|
|
if (normalized.Experience.Count > 0)
|
|
{
|
|
var block = new List<string>();
|
|
foreach (var item in normalized.Experience)
|
|
{
|
|
AddLine(block, item.Title);
|
|
var meta = string.Join(" | ", new[]
|
|
{
|
|
item.Company,
|
|
item.Location,
|
|
FormatDateRange(item.Start, item.End, item.IsCurrent)
|
|
}.Where(value => !string.IsNullOrWhiteSpace(value)));
|
|
AddLine(block, meta);
|
|
foreach (var bullet in item.Bullets)
|
|
{
|
|
AddLine(block, $"- {bullet}");
|
|
}
|
|
AddLine(block, string.Empty);
|
|
}
|
|
AddBlock(lines, "Experience", block);
|
|
}
|
|
|
|
if (normalized.Education.Count > 0)
|
|
{
|
|
var block = new List<string>();
|
|
foreach (var item in normalized.Education)
|
|
{
|
|
AddLine(block, string.IsNullOrWhiteSpace(item.QualificationLevel) ? item.Qualification : $"{item.Qualification} ({item.QualificationLevel})");
|
|
var meta = string.Join(" | ", new[]
|
|
{
|
|
item.Institution,
|
|
item.Location,
|
|
FormatDateRange(item.Start, item.End, false)
|
|
}.Where(value => !string.IsNullOrWhiteSpace(value)));
|
|
AddLine(block, meta);
|
|
foreach (var detail in item.Details)
|
|
{
|
|
AddLine(block, $"- {detail}");
|
|
}
|
|
AddLine(block, string.Empty);
|
|
}
|
|
AddBlock(lines, "Education", block);
|
|
}
|
|
|
|
foreach (var section in normalized.CustomSections)
|
|
{
|
|
AddBlock(lines, section.Title ?? "Additional Information", section.Items);
|
|
}
|
|
|
|
return string.Join("\n\n", lines.Where(line => !string.IsNullOrWhiteSpace(line)).Select(line => line.Trim())).Trim();
|
|
}
|
|
|
|
private static TailoredCvExperienceItem NormalizeExperience(TailoredCvExperienceItem? item)
|
|
{
|
|
item ??= new TailoredCvExperienceItem();
|
|
item.Title = TrimOrNull(item.Title);
|
|
item.Company = TrimOrNull(item.Company);
|
|
item.Location = TrimOrNull(item.Location);
|
|
item.Start = TrimOrNull(item.Start);
|
|
item.End = TrimOrNull(item.End);
|
|
item.Bullets = CleanList(item.Bullets);
|
|
item.IsCurrent = item.IsCurrent || string.Equals(item.End, "present", StringComparison.OrdinalIgnoreCase) || string.Equals(item.End, "current", StringComparison.OrdinalIgnoreCase);
|
|
return item;
|
|
}
|
|
|
|
private static TailoredCvEducationItem NormalizeEducation(TailoredCvEducationItem? item)
|
|
{
|
|
item ??= new TailoredCvEducationItem();
|
|
item.Qualification = TrimOrNull(item.Qualification);
|
|
item.QualificationLevel = TrimOrNull(item.QualificationLevel);
|
|
item.Institution = TrimOrNull(item.Institution);
|
|
item.Location = TrimOrNull(item.Location);
|
|
item.Start = TrimOrNull(item.Start);
|
|
item.End = TrimOrNull(item.End);
|
|
item.Details = CleanList(item.Details);
|
|
return item;
|
|
}
|
|
|
|
private static TailoredCvCustomSection NormalizeCustomSection(TailoredCvCustomSection? item)
|
|
{
|
|
item ??= new TailoredCvCustomSection();
|
|
item.Title = TrimOrNull(item.Title);
|
|
item.Items = CleanList(item.Items);
|
|
return item;
|
|
}
|
|
|
|
private static List<string> DeserializeList(string? json)
|
|
{
|
|
return DeserializeList<string>(json)
|
|
.Select(value => value?.Trim() ?? string.Empty)
|
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
}
|
|
|
|
private static List<T> DeserializeList<T>(string? json)
|
|
{
|
|
try
|
|
{
|
|
return string.IsNullOrWhiteSpace(json)
|
|
? new List<T>()
|
|
: JsonSerializer.Deserialize<List<T>>(json, SerializerOptions) ?? new List<T>();
|
|
}
|
|
catch
|
|
{
|
|
return new List<T>();
|
|
}
|
|
}
|
|
|
|
private static T? DeserializeObject<T>(string? json) where T : class
|
|
{
|
|
try
|
|
{
|
|
return string.IsNullOrWhiteSpace(json) ? null : JsonSerializer.Deserialize<T>(json, SerializerOptions);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static List<string> CleanList(IEnumerable<string>? values)
|
|
{
|
|
return (values ?? Array.Empty<string>())
|
|
.Select(value => value?.Trim() ?? string.Empty)
|
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
}
|
|
|
|
private static string? TrimOrNull(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
|
|
private static string? FormatDateRange(string? start, string? end, bool isCurrent)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(start) && string.IsNullOrWhiteSpace(end)) return null;
|
|
if (string.IsNullOrWhiteSpace(start)) return end;
|
|
return $"{start} - {(isCurrent ? "Present" : end ?? "Present")}";
|
|
}
|
|
|
|
private static void AddBlock(List<string> lines, string title, IEnumerable<string> body)
|
|
{
|
|
var content = string.Join("\n", body.Where(line => !string.IsNullOrWhiteSpace(line)).Select(line => line.Trim())).Trim();
|
|
if (string.IsNullOrWhiteSpace(content)) return;
|
|
lines.Add($"{title}\n{content}");
|
|
}
|
|
|
|
private static void AddLine(List<string> lines, string? value)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(value)) lines.Add(value.Trim());
|
|
}
|
|
}
|