using System.Text.Json; using System.Text.Json.Serialization; namespace JobTrackerApi.Models; // The "lens" a CvVariant stores. Deliberately all-optional + defaulted so an empty settings object // renders a sensible default CV straight from the master profile. Overrides are keyed by the master // profile's stable ItemKeys so career data is referenced, never copied. public sealed class CvVariantSettings { public string ThemeId { get; set; } = "modern"; // Theme overrides (null => use the theme's own value). public string? AccentColor { get; set; } public string? HeadingFont { get; set; } public string? BodyFont { get; set; } public string? Density { get; set; } // compact | balanced | roomy public string? PageSize { get; set; } // a4 | letter public string? DateFormat { get; set; } // display hint for the frontend; renderer uses source strings public string? Language { get; set; } public string? Headline { get; set; } // override the contact headline for this variant public bool ShowPhoto { get; set; } public bool ShowPageNumbers { get; set; } public bool ShowIcons { get; set; } = true; // Section order + visibility + optional renamed heading. Empty => default order, all visible. public List Sections { get; set; } = new(); // Per-item overrides keyed by ItemKey (job/education/project/certification stable id). public Dictionary Overrides { get; set; } = new(); // Extra sections unique to this variant (not in the master profile). public List CustomSections { get; set; } = new(); } public sealed class CvSectionSetting { public string Key { get; set; } = string.Empty; // summary|skills|experience|education|projects|certifications|languages|interests|links|custom: public bool Hidden { get; set; } public string? Title { get; set; } // renamed heading // Explicit entry order for this section, as a list of item ItemKeys. Entries not listed keep their // master order and come last. Null => master profile order. Lets a variant reorder entries without // touching the master profile. public List? ItemOrder { get; set; } } public sealed class CvItemOverride { public bool Hidden { get; set; } public string? Title { get; set; } public string? Subtitle { get; set; } public List? Bullets { get; set; } // null => use master bullets; non-null => replace for this variant only } public sealed class CvCustomSectionSetting { public string Key { get; set; } = string.Empty; public string? Title { get; set; } public List Items { get; set; } = new(); public bool Hidden { get; set; } } public static class CvVariantSettingsJson { private static readonly HashSet AllowedFonts = new(StringComparer.Ordinal) { "'Segoe UI', Roboto, Arial, sans-serif", "Arial, Helvetica, sans-serif", "Georgia, 'Times New Roman', serif", "'Helvetica Neue', Arial, sans-serif", "'Roboto', Arial, sans-serif", "'Poppins', 'Segoe UI', Arial, sans-serif", }; private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; public static string Serialize(CvVariantSettings? settings) => JsonSerializer.Serialize(Normalize(settings), Options); public static CvVariantSettings Deserialize(string? json) { if (string.IsNullOrWhiteSpace(json)) return new CvVariantSettings(); try { return Normalize(JsonSerializer.Deserialize(json, Options)); } catch { return new CvVariantSettings(); } } public static CvVariantSettings Normalize(CvVariantSettings? s) { s ??= new CvVariantSettings(); s.ThemeId = string.IsNullOrWhiteSpace(s.ThemeId) ? "modern" : s.ThemeId.Trim().ToLowerInvariant(); s.AccentColor = NormalizeColor(s.AccentColor); s.HeadingFont = NormalizeFont(s.HeadingFont); s.BodyFont = NormalizeFont(s.BodyFont); s.Sections ??= new(); s.Overrides ??= new(); s.CustomSections ??= new(); return s; } private static string? NormalizeColor(string? value) { var candidate = value?.Trim(); return candidate is { Length: 7 } && candidate[0] == '#' && candidate.Skip(1).All(Uri.IsHexDigit) ? candidate.ToLowerInvariant() : null; } private static string? NormalizeFont(string? value) { var candidate = value?.Trim(); return candidate is not null && AllowedFonts.Contains(candidate) ? candidate : null; } }