b203120ab4
Phase 3, API layer. GET/PUT /api/career/profile — the endpoint the /career editor uses to read and write the master profile. - GET: returns the structured profile (assembled from the relational children, backfilled from the blob if needed) plus a completeness scorecard. - PUT: validates limits, persists via CareerProfileService (relational children + append-only version), then serializes the result into ApplicationUser.ProfileCvStructureJson so the legacy read paths stay in sync. Identity fields are untouched (they belong to /profile). - GET /completeness: just the scorecard, for the overview. - CareerCompleteness: weighted percent + missing sections. - CareerProfileValidator: item-count/length limits (abuse guard, NOT completeness — a work-in-progress profile always saves). Tests (+4): put/get round-trip + projection sync, completeness, over-limit rejection, empty WIP profile accepted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
47 lines
2.0 KiB
C#
47 lines
2.0 KiB
C#
using JobTrackerApi.Controllers;
|
|
using JobTrackerApi.Models;
|
|
|
|
namespace JobTrackerApi.Services;
|
|
|
|
// Phase 3: the /career overview scorecard — "Profile completeness: 70%, missing: skills, projects".
|
|
// Weighted so the sections that most affect a CV (identity, experience) count for more.
|
|
public static class CareerCompleteness
|
|
{
|
|
private sealed record Section(string Key, string Label, int Weight, Func<StructuredCvProfile, int> Count);
|
|
|
|
private static readonly Section[] Sections =
|
|
{
|
|
new("personal", "Personal details", 20, p =>
|
|
(!string.IsNullOrWhiteSpace(p.Contact.FullName) ? 1 : 0)
|
|
+ (!string.IsNullOrWhiteSpace(p.Contact.Email) ? 1 : 0)),
|
|
new("summary", "Professional summary", 10, p => p.Summary.Count(s => !string.IsNullOrWhiteSpace(s))),
|
|
new("experience", "Experience", 25, p => p.Jobs.Count),
|
|
new("education", "Education", 15, p => p.Education.Count),
|
|
new("skills", "Skills", 15, p => p.Skills.Count(s => !string.IsNullOrWhiteSpace(s))),
|
|
new("projects", "Projects", 10, p => p.Projects.Count),
|
|
new("languages", "Languages", 5, p => p.Languages.Count),
|
|
};
|
|
|
|
public static CareerCompletenessDto Evaluate(StructuredCvProfile profile)
|
|
{
|
|
var statuses = new List<CareerSectionStatusDto>();
|
|
var missing = new List<string>();
|
|
var earned = 0;
|
|
var total = 0;
|
|
|
|
foreach (var s in Sections)
|
|
{
|
|
total += s.Weight;
|
|
var count = s.Count(profile);
|
|
// "personal" needs both name + email (count 2); the rest need at least one item.
|
|
var complete = s.Key == "personal" ? count >= 2 : count >= 1;
|
|
if (complete) earned += s.Weight;
|
|
else missing.Add(s.Label);
|
|
statuses.Add(new CareerSectionStatusDto(s.Key, s.Label, complete, count));
|
|
}
|
|
|
|
var percent = total == 0 ? 0 : (int)Math.Round(100.0 * earned / total);
|
|
return new CareerCompletenessDto(percent, missing, statuses);
|
|
}
|
|
}
|