feat(career): wire /career to the relational profile API + completeness overview
Phase 3, frontend. /career now reads and writes the master profile through the
relational source of truth instead of the legacy blob path.
- CareerProfilePage loads GET /career/profile (structured profile from the
relational children + cvText + completeness) and saves PUT /career/profile
({ profile, cvText }). This keeps the relational store authoritative — the
previous PUT /auth/profile blob write left it stale after first load.
- Added a "Profile completeness" overview (percent bar + missing sections) at the
top of /career, from the server scorecard.
- PUT /career/profile now accepts { profile, cvText } so the single /career save
covers both the structured profile and the raw imported text; GET returns cvText.
Tests: career-save asserts the /career/profile payload; new completeness-overview
test; controller tests updated for the request wrapper. 75/76 frontend pass (the
1 failure is the unrelated pre-existing settings-view suite); prod build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ public sealed class CareerProfileController : ControllerBase
|
||||
if (user is null) return Unauthorized();
|
||||
|
||||
var profile = await _career.LoadStructuredAsync(user.Id, cancellationToken);
|
||||
return Ok(new CareerProfileDto(profile, CareerCompleteness.Evaluate(profile)));
|
||||
return Ok(new CareerProfileDto(profile, CareerCompleteness.Evaluate(profile), user.ProfileCvText));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,23 +46,25 @@ public sealed class CareerProfileController : ControllerBase
|
||||
/// </summary>
|
||||
[HttpPut]
|
||||
[Authorize(AuthenticationSchemes = "local")]
|
||||
public async Task<ActionResult<CareerProfileDto>> Put([FromBody] StructuredCvProfile? request, CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<CareerProfileDto>> Put([FromBody] CareerProfileSaveRequest? request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _users.GetUserAsync(User);
|
||||
if (user is null) return StatusCode(501, "The career profile can only be edited on local accounts.");
|
||||
|
||||
var profile = StructuredCvProfileJson.Normalize(request);
|
||||
var profile = StructuredCvProfileJson.Normalize(request?.Profile);
|
||||
var error = CareerProfileValidator.Validate(profile);
|
||||
if (error is not null) return BadRequest(error);
|
||||
|
||||
var saved = await _career.SaveVersionAsync(user.Id, profile, "manual", cancellationToken);
|
||||
|
||||
// Keep the derived projection in sync for legacy readers.
|
||||
// Keep the derived projection in sync for legacy readers. CvText (the raw imported text) is
|
||||
// part of the career profile and is set here too; identity fields are never touched.
|
||||
user.ProfileCvStructureJson = StructuredCvProfileJson.Serialize(saved);
|
||||
if (request?.CvText is not null) user.ProfileCvText = string.IsNullOrWhiteSpace(request.CvText) ? null : request.CvText;
|
||||
var res = await _users.UpdateAsync(user);
|
||||
if (!res.Succeeded) return BadRequest(string.Join("; ", res.Errors.Select(e => e.Description)));
|
||||
|
||||
return Ok(new CareerProfileDto(saved, CareerCompleteness.Evaluate(saved)));
|
||||
return Ok(new CareerProfileDto(saved, CareerCompleteness.Evaluate(saved), user.ProfileCvText));
|
||||
}
|
||||
|
||||
/// <summary>Just the completeness scorecard, for the /career overview.</summary>
|
||||
@@ -76,7 +78,9 @@ public sealed class CareerProfileController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CareerProfileDto(StructuredCvProfile Profile, CareerCompletenessDto Completeness);
|
||||
public sealed record CareerProfileSaveRequest(StructuredCvProfile? Profile, string? CvText);
|
||||
|
||||
public sealed record CareerProfileDto(StructuredCvProfile Profile, CareerCompletenessDto Completeness, string? CvText);
|
||||
|
||||
public sealed record CareerCompletenessDto(int Percent, List<string> Missing, List<CareerSectionStatusDto> Sections);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user