From c33640986e6d5426fcede6b2e24dbb2058c16b8a Mon Sep 17 00:00:00 2001 From: cesnimda Date: Mon, 23 Mar 2026 23:48:39 +0100 Subject: [PATCH] Persist structured CV section data --- JobTrackerApi/Controllers/AuthController.cs | 8 +++++++- JobTrackerApi/Controllers/ProfileCvController.cs | 14 ++++++++++++++ JobTrackerApi/Program.cs | 2 ++ Models/ApplicationUser.cs | 1 + job-tracker-ui/src/pages/ProfilePage.tsx | 9 ++++++++- 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/JobTrackerApi/Controllers/AuthController.cs b/JobTrackerApi/Controllers/AuthController.cs index 6abf564..073b5a5 100644 --- a/JobTrackerApi/Controllers/AuthController.cs +++ b/JobTrackerApi/Controllers/AuthController.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Security.Claims; using JobTrackerApi.Models; using JobTrackerApi.Services; @@ -57,10 +58,11 @@ public sealed class AuthController : ControllerBase string? LastName, string? DisplayName, string? ProfileCvText, + string? ProfileCvStructureJson, string? AvatarImageDataUrl, IList Roles, 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); [HttpPost("login")] @@ -173,6 +175,7 @@ public sealed class AuthController : ControllerBase LastName: User.FindFirstValue("family_name"), DisplayName: User.FindFirstValue("name"), ProfileCvText: null, + ProfileCvStructureJson: null, AvatarImageDataUrl: null, Roles: Array.Empty(), GoogleLink: provider == "google" ? new GoogleLinkDto(false, email, null) : null)); @@ -194,6 +197,7 @@ public sealed class AuthController : ControllerBase var lastName = TrimOrNull(request.LastName); var displayName = TrimOrNull(request.DisplayName); var profileCvText = TrimOrNull(request.ProfileCvText); + var profileCvStructureJson = TrimOrNull(request.ProfileCvStructureJson); if (email is not null) user.Email = email; if (userName is not null) user.UserName = userName; @@ -201,6 +205,7 @@ public sealed class AuthController : ControllerBase user.LastName = lastName; user.DisplayName = displayName; user.ProfileCvText = profileCvText; + user.ProfileCvStructureJson = profileCvStructureJson; var res = await _users.UpdateAsync(user); if (!res.Succeeded) @@ -440,6 +445,7 @@ public sealed class AuthController : ControllerBase LastName: user.LastName, DisplayName: user.DisplayName, ProfileCvText: user.ProfileCvText, + ProfileCvStructureJson: user.ProfileCvStructureJson, AvatarImageDataUrl: user.AvatarImageDataUrl, Roles: roles, GoogleLink: new GoogleLinkDto( diff --git a/JobTrackerApi/Controllers/ProfileCvController.cs b/JobTrackerApi/Controllers/ProfileCvController.cs index adac1cc..6123617 100644 --- a/JobTrackerApi/Controllers/ProfileCvController.cs +++ b/JobTrackerApi/Controllers/ProfileCvController.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using JobTrackerApi.Services; using JobTrackerApi.Models; @@ -86,6 +87,8 @@ public sealed class ProfileCvController : ControllerBase } 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); if (!result.Succeeded) { @@ -114,6 +117,8 @@ public sealed class ProfileCvController : ControllerBase } 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); if (!result.Succeeded) { @@ -161,6 +166,13 @@ public sealed class ProfileCvController : ControllerBase .Select(section => new ParsedCvSectionDto(section.Name, section.Content, CountWords(section.Content))) .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) }); } @@ -183,6 +195,8 @@ public sealed class ProfileCvController : ControllerBase } 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); if (!result.Succeeded) { diff --git a/JobTrackerApi/Program.cs b/JobTrackerApi/Program.cs index 28330e6..790a1be 100644 --- a/JobTrackerApi/Program.cs +++ b/JobTrackerApi/Program.cs @@ -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", "DisplayName", "ALTER TABLE AspNetUsers ADD COLUMN DisplayName 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", "GoogleSubject", "ALTER TABLE AspNetUsers ADD COLUMN GoogleSubject 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, "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, "AspNetUsers", "ProfileCvStructureJson", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvStructureJson` longtext NULL;"); if (!MySqlIndexExists(conn, "Companies", "IX_Companies_OwnerUserId")) { diff --git a/Models/ApplicationUser.cs b/Models/ApplicationUser.cs index 5d5fe77..39b88b2 100644 --- a/Models/ApplicationUser.cs +++ b/Models/ApplicationUser.cs @@ -8,6 +8,7 @@ public sealed class ApplicationUser : IdentityUser public string? LastName { get; set; } public string? DisplayName { get; set; } public string? ProfileCvText { get; set; } + public string? ProfileCvStructureJson { get; set; } public string? AvatarImageDataUrl { get; set; } public string? GoogleSubject { get; set; } public string? GoogleEmail { get; set; } diff --git a/job-tracker-ui/src/pages/ProfilePage.tsx b/job-tracker-ui/src/pages/ProfilePage.tsx index 1c4a110..ebf5e43 100644 --- a/job-tracker-ui/src/pages/ProfilePage.tsx +++ b/job-tracker-ui/src/pages/ProfilePage.tsx @@ -29,6 +29,7 @@ type MeResponse = { lastName?: string; displayName?: string; profileCvText?: string; + profileCvStructureJson?: string; avatarImageDataUrl?: string; roles?: string[]; googleLink?: { @@ -115,6 +116,12 @@ export default function ProfilePage() { setLastName(r.data?.lastName ?? ""); setDisplayName(r.data?.displayName ?? ""); 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") ?? ""); } catch { setMe(null); @@ -467,7 +474,7 @@ export default function ProfilePage() { onClick={async () => { setLoading(true); 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()); await loadProfile(); toast(t("profileUpdated"), "success");