Harden production schema fallback and profile/dashboard UI
This commit is contained in:
@@ -78,9 +78,56 @@ public sealed class AdminSystemController : ControllerBase
|
|||||||
var dbPath = _paths.GetDbPath();
|
var dbPath = _paths.GetDbPath();
|
||||||
var dbFile = new FileInfo(dbPath);
|
var dbFile = new FileInfo(dbPath);
|
||||||
|
|
||||||
var jobs = await _db.JobApplications.AsNoTracking().ToListAsync(cancellationToken);
|
var jobCount = 0;
|
||||||
var companies = await _db.Companies.AsNoTracking().CountAsync(cancellationToken);
|
var deletedCount = 0;
|
||||||
var ai = await _summarizer.GetMetricsAsync(cancellationToken);
|
var companies = 0;
|
||||||
|
string? statusWarning = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jobCount = await _db.JobApplications.AsNoTracking().CountAsync(cancellationToken);
|
||||||
|
deletedCount = await _db.JobApplications.AsNoTracking().CountAsync(x => x.IsDeleted, cancellationToken);
|
||||||
|
companies = await _db.Companies.AsNoTracking().CountAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
statusWarning = $"Data query failed: {ex.GetType().Name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
AiServiceMetrics ai;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ai = await _summarizer.GetMetricsAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ai = new AiServiceMetrics(
|
||||||
|
Healthy: false,
|
||||||
|
Model: null,
|
||||||
|
Device: null,
|
||||||
|
GpuAvailable: false,
|
||||||
|
GpuName: null,
|
||||||
|
OcrAvailable: false,
|
||||||
|
OcrLanguages: null,
|
||||||
|
HealthLatencyMs: null,
|
||||||
|
ProbeLatencyMs: null,
|
||||||
|
LastProbeAt: null,
|
||||||
|
LastProbeSuccessAt: null,
|
||||||
|
LastProbeFailureAt: null,
|
||||||
|
ProbeFailures: 0,
|
||||||
|
Requests: 0,
|
||||||
|
CacheHits: 0,
|
||||||
|
CacheMisses: 0,
|
||||||
|
Failures: 0,
|
||||||
|
AverageLatencyMs: null,
|
||||||
|
OcrRequests: 0,
|
||||||
|
OcrFailures: 0,
|
||||||
|
AverageOcrLatencyMs: null,
|
||||||
|
LastOcrSuccessAt: null,
|
||||||
|
LastOcrFailureAt: null,
|
||||||
|
LastSuccessAt: null,
|
||||||
|
LastFailureAt: null,
|
||||||
|
LastError: ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
var version = NormalizeBuildMetadata(_cfg["App:Version"]);
|
var version = NormalizeBuildMetadata(_cfg["App:Version"]);
|
||||||
if (string.IsNullOrWhiteSpace(version))
|
if (string.IsNullOrWhiteSpace(version))
|
||||||
@@ -130,11 +177,18 @@ public sealed class AdminSystemController : ControllerBase
|
|||||||
{
|
{
|
||||||
dbWarning = "Database connection failed.";
|
dbWarning = "Database connection failed.";
|
||||||
}
|
}
|
||||||
else if (!usesFileStorage && jobs.Count == 0 && companies == 0)
|
else if (!usesFileStorage && jobCount == 0 && companies == 0)
|
||||||
{
|
{
|
||||||
dbWarning = "Connected, but no data is present yet. Check whether this is the intended production database.";
|
dbWarning = "Connected, but no data is present yet. Check whether this is the intended production database.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(statusWarning))
|
||||||
|
{
|
||||||
|
dbWarning = string.IsNullOrWhiteSpace(dbWarning)
|
||||||
|
? statusWarning
|
||||||
|
: $"{dbWarning} {statusWarning}";
|
||||||
|
}
|
||||||
|
|
||||||
var gmailConfigured = !string.IsNullOrWhiteSpace((_cfg["Google:GmailClientSecret"] ?? string.Empty).Trim())
|
var gmailConfigured = !string.IsNullOrWhiteSpace((_cfg["Google:GmailClientSecret"] ?? string.Empty).Trim())
|
||||||
&& !string.IsNullOrWhiteSpace((_cfg["Google:GmailRedirectUri"] ?? string.Empty).Trim());
|
&& !string.IsNullOrWhiteSpace((_cfg["Google:GmailRedirectUri"] ?? string.Empty).Trim());
|
||||||
|
|
||||||
@@ -150,8 +204,8 @@ public sealed class AdminSystemController : ControllerBase
|
|||||||
DbExists: dbFile.Exists,
|
DbExists: dbFile.Exists,
|
||||||
DbSizeBytes: dbFile.Exists ? dbFile.Length : null,
|
DbSizeBytes: dbFile.Exists ? dbFile.Length : null,
|
||||||
CompanyCount: companies,
|
CompanyCount: companies,
|
||||||
JobCount: jobs.Count,
|
JobCount: jobCount,
|
||||||
DeletedCount: jobs.Count(x => x.IsDeleted)
|
DeletedCount: deletedCount
|
||||||
),
|
),
|
||||||
Email: new EmailStatusDto(
|
Email: new EmailStatusDto(
|
||||||
Enabled: _cfg.GetValue("Email:Enabled", false),
|
Enabled: _cfg.GetValue("Email:Enabled", false),
|
||||||
|
|||||||
@@ -737,8 +737,39 @@ CREATE TABLE IF NOT EXISTS "GmailConnections" (
|
|||||||
}
|
}
|
||||||
|
|
||||||
EnsureMySqlColumn(conn, "Companies", "OwnerUserId", "ALTER TABLE `Companies` ADD COLUMN `OwnerUserId` varchar(255) NULL;");
|
EnsureMySqlColumn(conn, "Companies", "OwnerUserId", "ALTER TABLE `Companies` ADD COLUMN `OwnerUserId` varchar(255) NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Companies", "Source", "ALTER TABLE `Companies` ADD COLUMN `Source` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Companies", "RecruiterName", "ALTER TABLE `Companies` ADD COLUMN `RecruiterName` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Companies", "RecruiterEmail", "ALTER TABLE `Companies` ADD COLUMN `RecruiterEmail` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Companies", "RecruiterLinkedIn", "ALTER TABLE `Companies` ADD COLUMN `RecruiterLinkedIn` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Companies", "LastContactedAt", "ALTER TABLE `Companies` ADD COLUMN `LastContactedAt` datetime NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Companies", "NextContactAt", "ALTER TABLE `Companies` ADD COLUMN `NextContactAt` datetime NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Companies", "PipelineStage", "ALTER TABLE `Companies` ADD COLUMN `PipelineStage` longtext NULL;");
|
||||||
EnsureMySqlColumn(conn, "JobApplications", "OwnerUserId", "ALTER TABLE `JobApplications` ADD COLUMN `OwnerUserId` varchar(255) NULL;");
|
EnsureMySqlColumn(conn, "JobApplications", "OwnerUserId", "ALTER TABLE `JobApplications` ADD COLUMN `OwnerUserId` varchar(255) NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "IsDeleted", "ALTER TABLE `JobApplications` ADD COLUMN `IsDeleted` tinyint(1) NOT NULL DEFAULT 0;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "DeletedAt", "ALTER TABLE `JobApplications` ADD COLUMN `DeletedAt` datetime NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "Location", "ALTER TABLE `JobApplications` ADD COLUMN `Location` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "Salary", "ALTER TABLE `JobApplications` ADD COLUMN `Salary` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "NextAction", "ALTER TABLE `JobApplications` ADD COLUMN `NextAction` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "FollowUpAt", "ALTER TABLE `JobApplications` ADD COLUMN `FollowUpAt` datetime NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "FeedbackRequestedAt", "ALTER TABLE `JobApplications` ADD COLUMN `FeedbackRequestedAt` datetime NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "RecruiterMessageDraft", "ALTER TABLE `JobApplications` ADD COLUMN `RecruiterMessageDraft` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "ResponseReceived", "ALTER TABLE `JobApplications` ADD COLUMN `ResponseReceived` tinyint(1) NOT NULL DEFAULT 0;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "ResponseDate", "ALTER TABLE `JobApplications` ADD COLUMN `ResponseDate` datetime NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "Notes", "ALTER TABLE `JobApplications` ADD COLUMN `Notes` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "CoverLetterText", "ALTER TABLE `JobApplications` ADD COLUMN `CoverLetterText` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "JobUrl", "ALTER TABLE `JobApplications` ADD COLUMN `JobUrl` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "Description", "ALTER TABLE `JobApplications` ADD COLUMN `Description` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "TranslatedDescription", "ALTER TABLE `JobApplications` ADD COLUMN `TranslatedDescription` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "DescriptionLanguage", "ALTER TABLE `JobApplications` ADD COLUMN `DescriptionLanguage` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "Tags", "ALTER TABLE `JobApplications` ADD COLUMN `Tags` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "Deadline", "ALTER TABLE `JobApplications` ADD COLUMN `Deadline` datetime NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "ShortSummary", "ALTER TABLE `JobApplications` ADD COLUMN `ShortSummary` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "TailoredCvText", "ALTER TABLE `JobApplications` ADD COLUMN `TailoredCvText` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "JobApplications", "TailoredCvUpdatedAt", "ALTER TABLE `JobApplications` ADD COLUMN `TailoredCvUpdatedAt` datetime NULL;");
|
||||||
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, "Correspondences", "Subject", "ALTER TABLE `Correspondences` ADD COLUMN `Subject` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Correspondences", "Channel", "ALTER TABLE `Correspondences` ADD COLUMN `Channel` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "Correspondences", "ExternalMessageId", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalMessageId` longtext NULL;");
|
||||||
EnsureMySqlColumn(conn, "Correspondences", "ExternalThreadId", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalThreadId` longtext NULL;");
|
EnsureMySqlColumn(conn, "Correspondences", "ExternalThreadId", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalThreadId` longtext NULL;");
|
||||||
EnsureMySqlColumn(conn, "Correspondences", "ExternalFrom", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalFrom` longtext NULL;");
|
EnsureMySqlColumn(conn, "Correspondences", "ExternalFrom", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalFrom` longtext NULL;");
|
||||||
EnsureMySqlColumn(conn, "Correspondences", "ExternalTo", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalTo` longtext NULL;");
|
EnsureMySqlColumn(conn, "Correspondences", "ExternalTo", "ALTER TABLE `Correspondences` ADD COLUMN `ExternalTo` longtext NULL;");
|
||||||
@@ -747,6 +778,9 @@ CREATE TABLE IF NOT EXISTS "GmailConnections" (
|
|||||||
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvText` longtext NULL;");
|
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvText", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvText` longtext NULL;");
|
||||||
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvStructureJson", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvStructureJson` longtext NULL;");
|
EnsureMySqlColumn(conn, "AspNetUsers", "ProfileCvStructureJson", "ALTER TABLE `AspNetUsers` ADD COLUMN `ProfileCvStructureJson` longtext NULL;");
|
||||||
EnsureMySqlColumn(conn, "AspNetUsers", "AvatarImageDataUrl", "ALTER TABLE `AspNetUsers` ADD COLUMN `AvatarImageDataUrl` longtext NULL;");
|
EnsureMySqlColumn(conn, "AspNetUsers", "AvatarImageDataUrl", "ALTER TABLE `AspNetUsers` ADD COLUMN `AvatarImageDataUrl` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "AspNetUsers", "GoogleSubject", "ALTER TABLE `AspNetUsers` ADD COLUMN `GoogleSubject` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "AspNetUsers", "GoogleEmail", "ALTER TABLE `AspNetUsers` ADD COLUMN `GoogleEmail` longtext NULL;");
|
||||||
|
EnsureMySqlColumn(conn, "AspNetUsers", "GoogleLinkedAt", "ALTER TABLE `AspNetUsers` ADD COLUMN `GoogleLinkedAt` datetime NULL;");
|
||||||
|
|
||||||
if (!MySqlIndexExists(conn, "Companies", "IX_Companies_OwnerUserId"))
|
if (!MySqlIndexExists(conn, "Companies", "IX_Companies_OwnerUserId"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -231,13 +231,13 @@ export default function DashboardView() {
|
|||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "flex-start" }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "flex-start" }}>
|
||||||
<Box sx={{ maxWidth: 760 }}>
|
<Box sx={{ maxWidth: 760 }}>
|
||||||
<Typography variant="overline" sx={{ color: "primary.main", fontWeight: 800 }}>
|
<Typography variant="overline" sx={{ color: theme.palette.mode === "dark" ? alpha(theme.palette.primary.light, 0.95) : "primary.main", fontWeight: 800 }}>
|
||||||
{t("dashboardHeroLabel")}
|
{t("dashboardHeroLabel")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h4" sx={{ fontWeight: 950, mt: 0.5, letterSpacing: -0.6 }}>
|
<Typography variant="h4" sx={{ fontWeight: 950, mt: 0.5, letterSpacing: -0.6, color: theme.palette.mode === "dark" ? "common.white" : "text.primary" }}>
|
||||||
{t("dashboardOverviewTitle")}
|
{t("dashboardOverviewTitle")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" sx={{ color: "text.secondary", mt: 1.25, maxWidth: 680 }}>
|
<Typography variant="body1" sx={{ color: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.82) : "text.secondary", mt: 1.25, maxWidth: 680 }}>
|
||||||
{t("dashboardOverviewBody")}
|
{t("dashboardOverviewBody")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export const translations = {
|
|||||||
cropDialogZoom: "Zoom",
|
cropDialogZoom: "Zoom",
|
||||||
cropDialogSave: "Save image",
|
cropDialogSave: "Save image",
|
||||||
dashboardOverviewTitle: "Dashboard overview",
|
dashboardOverviewTitle: "Dashboard overview",
|
||||||
dashboardHeroLabel: "Jobbjakt Analytics",
|
dashboardHeroLabel: "Job search overview",
|
||||||
dashboardResponseRate: "{rate}% response rate",
|
dashboardResponseRate: "{rate}% response rate",
|
||||||
dashboardMonthsShort: "{count} mo",
|
dashboardMonthsShort: "{count} mo",
|
||||||
dashboardAppliedCount: "{count} applied",
|
dashboardAppliedCount: "{count} applied",
|
||||||
@@ -243,7 +243,7 @@ export const translations = {
|
|||||||
dashboardResponseConversion: "{responses}/{total} response conversion",
|
dashboardResponseConversion: "{responses}/{total} response conversion",
|
||||||
dashboardNoSourceData: "No source data yet.",
|
dashboardNoSourceData: "No source data yet.",
|
||||||
dashboardCompanyJobsResponses: "{jobs} jobs · {responses} responses",
|
dashboardCompanyJobsResponses: "{jobs} jobs · {responses} responses",
|
||||||
dashboardOverviewBody: "High-level application activity only. System health and pipeline diagnostics now live in the System page to avoid duplicated or conflicting status data.",
|
dashboardOverviewBody: "A quick view of your application activity, follow-ups, and momentum.",
|
||||||
dashboardCustomize: "Customize dashboard",
|
dashboardCustomize: "Customize dashboard",
|
||||||
dashboardSummaryCards: "Summary cards",
|
dashboardSummaryCards: "Summary cards",
|
||||||
dashboardActivityChart: "Activity chart",
|
dashboardActivityChart: "Activity chart",
|
||||||
@@ -1046,7 +1046,7 @@ export const translations = {
|
|||||||
cropDialogZoom: "Zoom",
|
cropDialogZoom: "Zoom",
|
||||||
cropDialogSave: "Lagre bilde",
|
cropDialogSave: "Lagre bilde",
|
||||||
dashboardOverviewTitle: "Dashboard-oversikt",
|
dashboardOverviewTitle: "Dashboard-oversikt",
|
||||||
dashboardHeroLabel: "Jobbjakt Analyse",
|
dashboardHeroLabel: "Oversikt over jobbsøket",
|
||||||
dashboardResponseRate: "{rate}% svarrate",
|
dashboardResponseRate: "{rate}% svarrate",
|
||||||
dashboardMonthsShort: "{count} md",
|
dashboardMonthsShort: "{count} md",
|
||||||
dashboardAppliedCount: "{count} søkt",
|
dashboardAppliedCount: "{count} søkt",
|
||||||
@@ -1054,7 +1054,7 @@ export const translations = {
|
|||||||
dashboardResponseConversion: "{responses}/{total} svar-konvertering",
|
dashboardResponseConversion: "{responses}/{total} svar-konvertering",
|
||||||
dashboardNoSourceData: "Ingen kildedata ennå.",
|
dashboardNoSourceData: "Ingen kildedata ennå.",
|
||||||
dashboardCompanyJobsResponses: "{jobs} jobber · {responses} svar",
|
dashboardCompanyJobsResponses: "{jobs} jobber · {responses} svar",
|
||||||
dashboardOverviewBody: "Kun overordnet aktivitet for jobbsøking vises her. Systemhelse og pipelinediagnostikk ligger nå på Systemsiden for å unngå dupliserte eller motstridende statusdata.",
|
dashboardOverviewBody: "En rask oversikt over aktivitet, oppfølginger og fremdrift i jobbsøket ditt.",
|
||||||
dashboardCustomize: "Tilpass dashboard",
|
dashboardCustomize: "Tilpass dashboard",
|
||||||
dashboardSummaryCards: "Oppsummeringskort",
|
dashboardSummaryCards: "Oppsummeringskort",
|
||||||
dashboardActivityChart: "Aktivitetsgraf",
|
dashboardActivityChart: "Aktivitetsgraf",
|
||||||
|
|||||||
@@ -118,7 +118,16 @@ export default function ProfilePage() {
|
|||||||
setProfileCvText(r.data?.profileCvText ?? "");
|
setProfileCvText(r.data?.profileCvText ?? "");
|
||||||
try {
|
try {
|
||||||
const parsed = r.data?.profileCvStructureJson ? JSON.parse(r.data.profileCvStructureJson) : [];
|
const parsed = r.data?.profileCvStructureJson ? JSON.parse(r.data.profileCvStructureJson) : [];
|
||||||
setParsedCvSections(Array.isArray(parsed) ? parsed : []);
|
const normalized = Array.isArray(parsed)
|
||||||
|
? parsed.map((section: any) => {
|
||||||
|
const content = typeof section?.content === "string" ? section.content : "";
|
||||||
|
const name = typeof section?.name === "string" && section.name.trim() ? section.name : t("profileCvSectionSummary");
|
||||||
|
const computedWordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
|
||||||
|
const wordCount = Number.isFinite(Number(section?.wordCount)) ? Number(section.wordCount) : computedWordCount;
|
||||||
|
return { name, content, wordCount };
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
setParsedCvSections(normalized);
|
||||||
} catch {
|
} catch {
|
||||||
setParsedCvSections([]);
|
setParsedCvSections([]);
|
||||||
}
|
}
|
||||||
@@ -363,7 +372,16 @@ export default function ProfilePage() {
|
|||||||
setParsingCvSections(true);
|
setParsingCvSections(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.post<{ sections?: ParsedCvSection[] }>("/profile-cv/parse", { text: profileCvText });
|
const res = await api.post<{ sections?: ParsedCvSection[] }>("/profile-cv/parse", { text: profileCvText });
|
||||||
setParsedCvSections(res.data?.sections ?? []);
|
const normalized = Array.isArray(res.data?.sections)
|
||||||
|
? res.data.sections.map((section: any) => {
|
||||||
|
const content = typeof section?.content === "string" ? section.content : "";
|
||||||
|
const name = typeof section?.name === "string" && section.name.trim() ? section.name : t("profileCvSectionSummary");
|
||||||
|
const computedWordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
|
||||||
|
const wordCount = Number.isFinite(Number(section?.wordCount)) ? Number(section.wordCount) : computedWordCount;
|
||||||
|
return { name, content, wordCount };
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
setParsedCvSections(normalized);
|
||||||
toast(t("profileCvStructureParsed"), "success");
|
toast(t("profileCvStructureParsed"), "success");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast(String(e?.response?.data || e?.message || t("profileCvStructureParseFailed")), "error");
|
toast(String(e?.response?.data || e?.message || t("profileCvStructureParseFailed")), "error");
|
||||||
@@ -377,15 +395,18 @@ export default function ProfilePage() {
|
|||||||
</Box>
|
</Box>
|
||||||
{parsedCvSections.length > 0 ? (
|
{parsedCvSections.length > 0 ? (
|
||||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
||||||
{parsedCvSections.map((section) => (
|
{parsedCvSections.map((section) => {
|
||||||
|
const safeContent = typeof section.content === "string" ? section.content : "";
|
||||||
|
const safeWordCount = Number.isFinite(Number(section.wordCount)) ? Number(section.wordCount) : (safeContent.trim() ? safeContent.trim().split(/\s+/).length : 0);
|
||||||
|
return (
|
||||||
<Box key={section.name} sx={{ p: 1.25, borderRadius: 2.5, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
<Box key={section.name} sx={{ p: 1.25, borderRadius: 2.5, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", mb: 0.75 }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", mb: 0.75 }}>
|
||||||
<Typography variant="overline">{section.name}</Typography>
|
<Typography variant="overline">{section.name}</Typography>
|
||||||
<Chip size="small" label={t("profileCvSectionWordCount", { count: section.wordCount })} />
|
<Chip size="small" label={t("profileCvSectionWordCount", { count: safeWordCount })} />
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2" sx={{ color: "text.secondary", whiteSpace: "pre-wrap" }}>{section.content.slice(0, 280)}{section.content.length > 280 ? "…" : ""}</Typography>
|
<Typography variant="body2" sx={{ color: "text.secondary", whiteSpace: "pre-wrap" }}>{safeContent.slice(0, 280)}{safeContent.length > 280 ? "…" : ""}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
)})}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvStructureEmpty")}</Typography>
|
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvStructureEmpty")}</Typography>
|
||||||
|
|||||||
Reference in New Issue
Block a user