Persist structured CV section data

This commit is contained in:
cesnimda
2026-03-23 23:48:39 +01:00
parent 8f04637cff
commit c33640986e
5 changed files with 32 additions and 2 deletions
+7 -1
View File
@@ -1,3 +1,4 @@
using System.Text.Json;
using System.Security.Claims; using System.Security.Claims;
using JobTrackerApi.Models; using JobTrackerApi.Models;
using JobTrackerApi.Services; using JobTrackerApi.Services;
@@ -57,10 +58,11 @@ public sealed class AuthController : ControllerBase
string? LastName, string? LastName,
string? DisplayName, string? DisplayName,
string? ProfileCvText, string? ProfileCvText,
string? ProfileCvStructureJson,
string? AvatarImageDataUrl, string? AvatarImageDataUrl,
IList<string> Roles, IList<string> Roles,
GoogleLinkDto? GoogleLink); GoogleLinkDto? GoogleLink);
public sealed record UpdateProfileRequest(string? Email, string? UserName, string? FirstName, string? LastName, string? DisplayName, string? ProfileCvText); public sealed record UpdateProfileRequest(string? Email, string? UserName, string? FirstName, string? LastName, string? DisplayName, string? ProfileCvText, string? ProfileCvStructureJson);
public sealed record GoogleTokenRequest(string Token); public sealed record GoogleTokenRequest(string Token);
[HttpPost("login")] [HttpPost("login")]
@@ -173,6 +175,7 @@ public sealed class AuthController : ControllerBase
LastName: User.FindFirstValue("family_name"), LastName: User.FindFirstValue("family_name"),
DisplayName: User.FindFirstValue("name"), DisplayName: User.FindFirstValue("name"),
ProfileCvText: null, ProfileCvText: null,
ProfileCvStructureJson: null,
AvatarImageDataUrl: null, AvatarImageDataUrl: null,
Roles: Array.Empty<string>(), Roles: Array.Empty<string>(),
GoogleLink: provider == "google" ? new GoogleLinkDto(false, email, null) : null)); GoogleLink: provider == "google" ? new GoogleLinkDto(false, email, null) : null));
@@ -194,6 +197,7 @@ public sealed class AuthController : ControllerBase
var lastName = TrimOrNull(request.LastName); var lastName = TrimOrNull(request.LastName);
var displayName = TrimOrNull(request.DisplayName); var displayName = TrimOrNull(request.DisplayName);
var profileCvText = TrimOrNull(request.ProfileCvText); var profileCvText = TrimOrNull(request.ProfileCvText);
var profileCvStructureJson = TrimOrNull(request.ProfileCvStructureJson);
if (email is not null) user.Email = email; if (email is not null) user.Email = email;
if (userName is not null) user.UserName = userName; if (userName is not null) user.UserName = userName;
@@ -201,6 +205,7 @@ public sealed class AuthController : ControllerBase
user.LastName = lastName; user.LastName = lastName;
user.DisplayName = displayName; user.DisplayName = displayName;
user.ProfileCvText = profileCvText; user.ProfileCvText = profileCvText;
user.ProfileCvStructureJson = profileCvStructureJson;
var res = await _users.UpdateAsync(user); var res = await _users.UpdateAsync(user);
if (!res.Succeeded) if (!res.Succeeded)
@@ -440,6 +445,7 @@ public sealed class AuthController : ControllerBase
LastName: user.LastName, LastName: user.LastName,
DisplayName: user.DisplayName, DisplayName: user.DisplayName,
ProfileCvText: user.ProfileCvText, ProfileCvText: user.ProfileCvText,
ProfileCvStructureJson: user.ProfileCvStructureJson,
AvatarImageDataUrl: user.AvatarImageDataUrl, AvatarImageDataUrl: user.AvatarImageDataUrl,
Roles: roles, Roles: roles,
GoogleLink: new GoogleLinkDto( GoogleLink: new GoogleLinkDto(
@@ -1,4 +1,5 @@
using System.Text; using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using JobTrackerApi.Services; using JobTrackerApi.Services;
using JobTrackerApi.Models; using JobTrackerApi.Models;
@@ -86,6 +87,8 @@ public sealed class ProfileCvController : ControllerBase
} }
user.ProfileCvText = text; user.ProfileCvText = text;
user.ProfileCvStructureJson = JsonSerializer.Serialize(
ParseSections(text).Select(section => new ParsedCvSectionDto(section.Name, section.Content, CountWords(section.Content))).ToList());
var result = await _users.UpdateAsync(user); var result = await _users.UpdateAsync(user);
if (!result.Succeeded) if (!result.Succeeded)
{ {
@@ -114,6 +117,8 @@ public sealed class ProfileCvController : ControllerBase
} }
user.ProfileCvText = rebuilt.Trim(); user.ProfileCvText = rebuilt.Trim();
user.ProfileCvStructureJson = JsonSerializer.Serialize(
ParseSections(user.ProfileCvText).Select(section => new ParsedCvSectionDto(section.Name, section.Content, CountWords(section.Content))).ToList());
var result = await _users.UpdateAsync(user); var result = await _users.UpdateAsync(user);
if (!result.Succeeded) if (!result.Succeeded)
{ {
@@ -161,6 +166,13 @@ public sealed class ProfileCvController : ControllerBase
.Select(section => new ParsedCvSectionDto(section.Name, section.Content, CountWords(section.Content))) .Select(section => new ParsedCvSectionDto(section.Name, section.Content, CountWords(section.Content)))
.ToList(); .ToList();
user.ProfileCvStructureJson = JsonSerializer.Serialize(sections);
var update = await _users.UpdateAsync(user);
if (!update.Succeeded)
{
return BadRequest(string.Join("; ", update.Errors.Select(e => e.Description)));
}
return Ok(new { sections, totalWords = CountWords(source) }); return Ok(new { sections, totalWords = CountWords(source) });
} }
@@ -183,6 +195,8 @@ public sealed class ProfileCvController : ControllerBase
} }
user.ProfileCvText = improved.Trim(); user.ProfileCvText = improved.Trim();
user.ProfileCvStructureJson = JsonSerializer.Serialize(
ParseSections(user.ProfileCvText).Select(section => new ParsedCvSectionDto(section.Name, section.Content, CountWords(section.Content))).ToList());
var result = await _users.UpdateAsync(user); var result = await _users.UpdateAsync(user);
if (!result.Succeeded) if (!result.Succeeded)
{ {
+2
View File
@@ -576,6 +576,7 @@ CREATE TABLE IF NOT EXISTS "AspNetUserTokens" (
EnsureColumn(conn, "AspNetUsers", "LastName", "ALTER TABLE AspNetUsers ADD COLUMN LastName TEXT NULL;"); EnsureColumn(conn, "AspNetUsers", "LastName", "ALTER TABLE AspNetUsers ADD COLUMN LastName TEXT NULL;");
EnsureColumn(conn, "AspNetUsers", "DisplayName", "ALTER TABLE AspNetUsers ADD COLUMN DisplayName TEXT NULL;"); EnsureColumn(conn, "AspNetUsers", "DisplayName", "ALTER TABLE AspNetUsers ADD COLUMN DisplayName TEXT NULL;");
EnsureColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE AspNetUsers ADD COLUMN ProfileCvText TEXT NULL;"); EnsureColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE AspNetUsers ADD COLUMN ProfileCvText TEXT NULL;");
EnsureColumn(conn, "AspNetUsers", "ProfileCvStructureJson", "ALTER TABLE AspNetUsers ADD COLUMN ProfileCvStructureJson TEXT NULL;");
EnsureColumn(conn, "AspNetUsers", "AvatarImageDataUrl", "ALTER TABLE AspNetUsers ADD COLUMN AvatarImageDataUrl TEXT NULL;"); EnsureColumn(conn, "AspNetUsers", "AvatarImageDataUrl", "ALTER TABLE AspNetUsers ADD COLUMN AvatarImageDataUrl TEXT NULL;");
EnsureColumn(conn, "AspNetUsers", "GoogleSubject", "ALTER TABLE AspNetUsers ADD COLUMN GoogleSubject TEXT NULL;"); EnsureColumn(conn, "AspNetUsers", "GoogleSubject", "ALTER TABLE AspNetUsers ADD COLUMN GoogleSubject TEXT NULL;");
EnsureColumn(conn, "AspNetUsers", "GoogleEmail", "ALTER TABLE AspNetUsers ADD COLUMN GoogleEmail TEXT NULL;"); EnsureColumn(conn, "AspNetUsers", "GoogleEmail", "ALTER TABLE AspNetUsers ADD COLUMN GoogleEmail TEXT NULL;");
@@ -734,6 +735,7 @@ CREATE TABLE IF NOT EXISTS "GmailConnections" (
EnsureMySqlColumn(conn, "JobApplications", "LastReminderEmailSentAt", "ALTER TABLE `JobApplications` ADD COLUMN `LastReminderEmailSentAt` datetime NULL;"); EnsureMySqlColumn(conn, "JobApplications", "LastReminderEmailSentAt", "ALTER TABLE `JobApplications` ADD COLUMN `LastReminderEmailSentAt` datetime NULL;");
EnsureMySqlColumn(conn, "Attachments", "Purpose", "ALTER TABLE `Attachments` ADD COLUMN `Purpose` varchar(100) NULL;"); EnsureMySqlColumn(conn, "Attachments", "Purpose", "ALTER TABLE `Attachments` ADD COLUMN `Purpose` varchar(100) NULL;");
EnsureMySqlColumn(conn, "Attachments", "UseForAi", "ALTER TABLE `Attachments` ADD COLUMN `UseForAi` tinyint(1) NOT NULL DEFAULT 1;"); EnsureMySqlColumn(conn, "Attachments", "UseForAi", "ALTER TABLE `Attachments` ADD COLUMN `UseForAi` tinyint(1) NOT NULL DEFAULT 1;");
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvStructureJson", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvStructureJson` longtext NULL;");
if (!MySqlIndexExists(conn, "Companies", "IX_Companies_OwnerUserId")) if (!MySqlIndexExists(conn, "Companies", "IX_Companies_OwnerUserId"))
{ {
+1
View File
@@ -8,6 +8,7 @@ public sealed class ApplicationUser : IdentityUser
public string? LastName { get; set; } public string? LastName { get; set; }
public string? DisplayName { get; set; } public string? DisplayName { get; set; }
public string? ProfileCvText { get; set; } public string? ProfileCvText { get; set; }
public string? ProfileCvStructureJson { get; set; }
public string? AvatarImageDataUrl { get; set; } public string? AvatarImageDataUrl { get; set; }
public string? GoogleSubject { get; set; } public string? GoogleSubject { get; set; }
public string? GoogleEmail { get; set; } public string? GoogleEmail { get; set; }
+8 -1
View File
@@ -29,6 +29,7 @@ type MeResponse = {
lastName?: string; lastName?: string;
displayName?: string; displayName?: string;
profileCvText?: string; profileCvText?: string;
profileCvStructureJson?: string;
avatarImageDataUrl?: string; avatarImageDataUrl?: string;
roles?: string[]; roles?: string[];
googleLink?: { googleLink?: {
@@ -115,6 +116,12 @@ export default function ProfilePage() {
setLastName(r.data?.lastName ?? ""); setLastName(r.data?.lastName ?? "");
setDisplayName(r.data?.displayName ?? ""); setDisplayName(r.data?.displayName ?? "");
setProfileCvText(r.data?.profileCvText ?? ""); setProfileCvText(r.data?.profileCvText ?? "");
try {
const parsed = r.data?.profileCvStructureJson ? JSON.parse(r.data.profileCvStructureJson) : [];
setParsedCvSections(Array.isArray(parsed) ? parsed : []);
} catch {
setParsedCvSections([]);
}
setHeadline(window.localStorage.getItem("profileHeadline") ?? ""); setHeadline(window.localStorage.getItem("profileHeadline") ?? "");
} catch { } catch {
setMe(null); setMe(null);
@@ -467,7 +474,7 @@ export default function ProfilePage() {
onClick={async () => { onClick={async () => {
setLoading(true); setLoading(true);
try { try {
await api.put("/auth/profile", { email, userName, firstName, lastName, displayName, profileCvText }); await api.put("/auth/profile", { email, userName, firstName, lastName, displayName, profileCvText, profileCvStructureJson: JSON.stringify(parsedCvSections) });
window.localStorage.setItem("profileHeadline", headline.trim()); window.localStorage.setItem("profileHeadline", headline.trim());
await loadProfile(); await loadProfile();
toast(t("profileUpdated"), "success"); toast(t("profileUpdated"), "success");