fix(i18n): localize application workflows
Use stable checklist and interview keys for system copy while preserving user-authored content. Localize creation-tab errors, language selectors, match bands, dates, and selected-CV context.
This commit is contained in:
@@ -420,4 +420,27 @@ public sealed class ApplicationIntelligenceTests
|
|||||||
Assert.Contains("C#", match.MatchedSkills);
|
Assert.Contains("C#", match.MatchedSkills);
|
||||||
Assert.DoesNotContain(match.MissingSkills, value => value.Equals("utvikler", StringComparison.OrdinalIgnoreCase));
|
Assert.DoesNotContain(match.MissingSkills, value => value.Equals("utvikler", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_reports_language_mismatch_when_no_translation_is_available()
|
||||||
|
{
|
||||||
|
var (db, intelligence, _) = New("user-1");
|
||||||
|
await using var _d = db;
|
||||||
|
var job = await SeedJobAsync(db, "user-1", j =>
|
||||||
|
{
|
||||||
|
j.Description = "Vi søker en utvikler som kan bygge robuste systemer og gode tjenester.";
|
||||||
|
j.DescriptionLanguage = null; // Exercise detector fallback for manually created jobs.
|
||||||
|
j.TranslatedDescription = null;
|
||||||
|
});
|
||||||
|
await SeedProfileAsync(db, "user-1");
|
||||||
|
await AttachCvAsync(db, "user-1", job.Id, new CvVariantSettings { Language = "en" });
|
||||||
|
|
||||||
|
var match = await intelligence.MatchAsync("user-1", job.Id, default);
|
||||||
|
|
||||||
|
Assert.NotNull(match);
|
||||||
|
Assert.Equal("nb", match!.JobLanguage);
|
||||||
|
Assert.Equal("en", match.CvLanguage);
|
||||||
|
Assert.True(match.LanguageMismatch);
|
||||||
|
Assert.False(match.UsedTranslatedJobDescription);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ Updated: 2026-08-29
|
|||||||
- Improved the Job Workspace overview card grid so compact screens use one readable column, tablets use two, and wide screens use five without leaving a cramped orphan card.
|
- Improved the Job Workspace overview card grid so compact screens use one readable column, tablets use two, and wide screens use five without leaving a cramped orphan card.
|
||||||
- Moved the remaining shared shell navigation labels into the global locale catalogue so sidebar and mobile-navigation controls announce themselves in the selected language.
|
- Moved the remaining shared shell navigation labels into the global locale catalogue so sidebar and mobile-navigation controls announce themselves in the selected language.
|
||||||
- Localized the application checklist from stable system keys rather than persisted English text. Categories, generated tasks, descriptions, progress, errors, tooltips, and accessible reorder/delete names now switch between English and Bokmål while user-authored task text remains untouched.
|
- Localized the application checklist from stable system keys rather than persisted English text. Categories, generated tasks, descriptions, progress, errors, tooltips, and accessible reorder/delete names now switch between English and Bokmål while user-authored task text remains untouched.
|
||||||
|
- Localized Cover Letter, CV attachment, application-draft, and Interview Prep failure states and document-language selectors. Interview categories now derive from stable category keys while user-authored questions and answers remain unchanged.
|
||||||
|
- Reused the checklist's stable system-key translation mapping for the Overview's recommended next action, preventing the English backend seed text from leaking into a Bokmål workspace.
|
||||||
|
- Corrected the CV Match source caption to interpolate the selected CV name once, localized known match bands, and made intelligence/timeline load failures and dates respect the selected UI locale.
|
||||||
|
|
||||||
### In progress
|
### In progress
|
||||||
|
|
||||||
@@ -61,6 +64,10 @@ Updated: 2026-08-29
|
|||||||
- Career/Profile focused verification: 2 suites, 19/19 passed, including the final navigation/state-preservation regression; final full-suite/E2E gates remain pending.
|
- Career/Profile focused verification: 2 suites, 19/19 passed, including the final navigation/state-preservation regression; final full-suite/E2E gates remain pending.
|
||||||
- Job Workspace focused verification: 2 suites, 9/9 passed; TypeScript passed.
|
- Job Workspace focused verification: 2 suites, 9/9 passed; TypeScript passed.
|
||||||
- Checklist localization verification: 7/7 passed, including a Bokmål regression that preserves user-authored content.
|
- Checklist localization verification: 7/7 passed, including a Bokmål regression that preserves user-authored content.
|
||||||
|
- Cover Letter/CV assets and Interview Prep focused verification: 2 suites, 27/27 passed, including Bokmål category rendering that preserves user content.
|
||||||
|
- Workspace/checklist integration verification: 2 suites, 14/14 passed, including localized system-derived next actions.
|
||||||
|
- Application intelligence focused verification: 11/11 passed; TypeScript passed after the creation-tab/localization batch.
|
||||||
|
- Backend matcher/intelligence focused verification: 35/35 passed, including detection of a manually created Norwegian advert with no saved translation.
|
||||||
- Full backend: 712/712 tests passed on .NET 9.
|
- Full backend: 712/712 tests passed on .NET 9.
|
||||||
- Next optimized production build and TypeScript: passed after the Job Workspace/checklist batch.
|
- Next optimized production build and TypeScript: passed after the Job Workspace/checklist batch.
|
||||||
- Playwright: 8/10 passed on the first complete run; both failures were ambiguous selectors in the newly responsive Career selector, not product failures. Both corrected focused regressions now pass (2/2); final full rerun remains in the end-of-batch gate.
|
- Playwright: 8/10 passed on the first complete run; both failures were ambiguous selectors in the newly responsive Career selector, not product failures. Both corrected focused regressions now pass (2/2); final full rerun remains in the end-of-batch gate.
|
||||||
|
|||||||
@@ -233,6 +233,18 @@ test("warns before section navigation would discard application edits", async ()
|
|||||||
|
|
||||||
test("renders shared workspace navigation and progress in Norwegian Bokmål", async () => {
|
test("renders shared workspace navigation and progress in Norwegian Bokmål", async () => {
|
||||||
window.localStorage.setItem("uiLanguage", "nb");
|
window.localStorage.setItem("uiLanguage", "nb");
|
||||||
|
mockedApi.get.mockImplementation((url: string) => {
|
||||||
|
if (url === "/jobapplications/42/workspace") return Promise.resolve({ data: {
|
||||||
|
...overview,
|
||||||
|
nextStep: {
|
||||||
|
key: "prepare-cv",
|
||||||
|
label: "Prepare a CV for this role",
|
||||||
|
reason: "Attach a CV variant tailored to this application.",
|
||||||
|
section: "cv",
|
||||||
|
},
|
||||||
|
} } as any);
|
||||||
|
return Promise.resolve({ data: [] } as any);
|
||||||
|
});
|
||||||
renderTable("/jobs/42");
|
renderTable("/jobs/42");
|
||||||
|
|
||||||
expect(await screen.findByRole("tab", { name: "Oversikt" })).toHaveAttribute("aria-selected", "true");
|
expect(await screen.findByRole("tab", { name: "Oversikt" })).toHaveAttribute("aria-selected", "true");
|
||||||
@@ -240,6 +252,7 @@ test("renders shared workspace navigation and progress in Norwegian Bokmål", as
|
|||||||
expect(screen.getByRole("tab", { name: "Søknadsbrev" })).toBeInTheDocument();
|
expect(screen.getByRole("tab", { name: "Søknadsbrev" })).toBeInTheDocument();
|
||||||
expect(await screen.findByLabelText(/søknadsprogresjon/i)).toBeInTheDocument();
|
expect(await screen.findByLabelText(/søknadsprogresjon/i)).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: /tilbake til søknader/i })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /tilbake til søknader/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("heading", { name: "Forbered en CV for denne stillingen" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hydrates list filters, sort and page from a shareable URL", async () => {
|
test("hydrates list filters, sort and page from a shareable URL", async () => {
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ export const WORKSPACE_SECTIONS: { key: WorkspaceSectionKey; label: string }[] =
|
|||||||
{ key: "interview", label: "Interview Prep" },
|
{ key: "interview", label: "Interview Prep" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function checklistSystemTranslationToken(systemKey?: string | null): string | null {
|
||||||
|
if (!systemKey || systemKey.startsWith("learning:")) return null;
|
||||||
|
return systemKey.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
const LEGACY_WORKSPACE_SECTIONS: Record<string, WorkspaceSectionKey> = {
|
const LEGACY_WORKSPACE_SECTIONS: Record<string, WorkspaceSectionKey> = {
|
||||||
match: "analysis",
|
match: "analysis",
|
||||||
"job-details": "overview",
|
"job-details": "overview",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useI18n } from "../i18n/I18nProvider";
|
|||||||
// is application-specific by nature. docs/architecture/application-workspace.md.
|
// is application-specific by nature. docs/architecture/application-workspace.md.
|
||||||
|
|
||||||
function useAsset<T>(load: () => Promise<T>, deps: React.DependencyList) {
|
function useAsset<T>(load: () => Promise<T>, deps: React.DependencyList) {
|
||||||
|
const { t } = useI18n();
|
||||||
const [data, setData] = useState<T | null>(null);
|
const [data, setData] = useState<T | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -44,7 +45,7 @@ function useAsset<T>(load: () => Promise<T>, deps: React.DependencyList) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!cancelled) setError(getApiErrorMessage(err, "Could not load this section."));
|
if (!cancelled) setError(getApiErrorMessage(err, t("assetsLoadFailed")));
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!cancelled) setLoading(false);
|
if (!cancelled) setLoading(false);
|
||||||
@@ -52,7 +53,7 @@ function useAsset<T>(load: () => Promise<T>, deps: React.DependencyList) {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [run]);
|
}, [run, t]);
|
||||||
|
|
||||||
useEffect(() => reload(), [reload]);
|
useEffect(() => reload(), [reload]);
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ export function ApplicationCvSection({ jobId }: { jobId: number }) {
|
|||||||
setData(await applicationAssetsApi.attachVariant(jobId, variantId));
|
setData(await applicationAssetsApi.attachVariant(jobId, variantId));
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not change the attached CV."));
|
setError(getApiErrorMessage(err, t("assetsAttachCvFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -110,11 +111,11 @@ export function ApplicationCvSection({ jobId }: { jobId: number }) {
|
|||||||
if (!data?.attachedVariantId) return;
|
if (!data?.attachedVariantId) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const copy = await cvBuilderApi.duplicate(data.attachedVariantId, `${data.attachedVariantName || "CV"} — tailored copy`);
|
const copy = await cvBuilderApi.duplicate(data.attachedVariantId, t("assetsTailoredCopyName", { name: data.attachedVariantName || "CV" }));
|
||||||
setData(await applicationAssetsApi.attachVariant(jobId, copy.id));
|
setData(await applicationAssetsApi.attachVariant(jobId, copy.id));
|
||||||
window.location.assign(`/career/builder/${copy.id}`);
|
window.location.assign(`/career/builder/${copy.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not create a tailored CV copy."));
|
setError(getApiErrorMessage(err, t("assetsDuplicateCvFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -308,7 +309,7 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
|
|||||||
setDraftAiAction(null);
|
setDraftAiAction(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not save the cover letter."));
|
setError(getApiErrorMessage(err, t("assetsCoverSaveFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -322,7 +323,7 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
|
|||||||
setDraftAiAction(null);
|
setDraftAiAction(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not restore that version."));
|
setError(getApiErrorMessage(err, t("assetsCoverRestoreFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -394,7 +395,7 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
|
|||||||
{v.isCurrent && <Chip size="small" color="primary" variant="outlined" label={t("assetsCurrent")} />}
|
{v.isCurrent && <Chip size="small" color="primary" variant="outlined" label={t("assetsCurrent")} />}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{new Date(v.createdAtUtc).toLocaleString()} · {t("assetsCharacterCount", { count: v.length })}
|
{new Date(v.createdAtUtc).toLocaleString(language === "nb" ? "nb-NO" : "en")} · {t("assetsCharacterCount", { count: v.length })}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{!v.isCurrent && (
|
{!v.isCurrent && (
|
||||||
@@ -458,7 +459,7 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
|
|||||||
});
|
});
|
||||||
setSuggestion(result.result.text?.trim() ?? "");
|
setSuggestion(result.result.text?.trim() ?? "");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not generate a cover-letter suggestion."));
|
setError(getApiErrorMessage(err, t("coverAiGenerateFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -499,8 +500,8 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
|
|||||||
<FormControl size="small" sx={{ minWidth: 190 }}>
|
<FormControl size="small" sx={{ minWidth: 190 }}>
|
||||||
<InputLabel>{t("coverAiDocumentLanguage")}</InputLabel>
|
<InputLabel>{t("coverAiDocumentLanguage")}</InputLabel>
|
||||||
<Select label={t("coverAiDocumentLanguage")} value={language} onChange={(event) => setLanguage(event.target.value as "en" | "nb-NO")}>
|
<Select label={t("coverAiDocumentLanguage")} value={language} onChange={(event) => setLanguage(event.target.value as "en" | "nb-NO")}>
|
||||||
<MenuItem value="en">English</MenuItem>
|
<MenuItem value="en">{t("languageEnglish")}</MenuItem>
|
||||||
<MenuItem value="nb-NO">Norsk bokmål</MenuItem>
|
<MenuItem value="nb-NO">{t("languageNorwegianBokmal")}</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -598,7 +599,7 @@ export function ApplicationPackageDraftsSection({
|
|||||||
setError(null);
|
setError(null);
|
||||||
onSaved?.();
|
onSaved?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not save the application drafts."));
|
setError(getApiErrorMessage(err, t("assetsDraftsSaveFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
|||||||
import { getApiErrorMessage } from "../api";
|
import { getApiErrorMessage } from "../api";
|
||||||
import { useI18n } from "../i18n/I18nProvider";
|
import { useI18n } from "../i18n/I18nProvider";
|
||||||
import {
|
import {
|
||||||
CHECKLIST_CATEGORIES, Checklist, ChecklistItem, applicationChecklistApi,
|
CHECKLIST_CATEGORIES, Checklist, ChecklistItem, applicationChecklistApi, checklistSystemTranslationToken,
|
||||||
} from "../applicationWorkspace";
|
} from "../applicationWorkspace";
|
||||||
|
|
||||||
// Phase 5 Milestone 2 — the application checklist.
|
// Phase 5 Milestone 2 — the application checklist.
|
||||||
@@ -214,8 +214,8 @@ function checklistCategoryLabel(t: (key: any, vars?: Record<string, string | num
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checklistItemDisplay(t: (key: any, vars?: Record<string, string | number>) => string, item: ChecklistItem) {
|
function checklistItemDisplay(t: (key: any, vars?: Record<string, string | number>) => string, item: ChecklistItem) {
|
||||||
if (!item.systemKey || item.systemKey.startsWith("learning:")) return { title: item.title, description: item.description };
|
const token = checklistSystemTranslationToken(item.systemKey);
|
||||||
const token = item.systemKey.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
if (!token) return { title: item.title, description: item.description };
|
||||||
return {
|
return {
|
||||||
title: t(`checklistItem_${token}`),
|
title: t(`checklistItem_${token}`),
|
||||||
description: t(`checklistItem_${token}Description`),
|
description: t(`checklistItem_${token}Description`),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { useI18n } from "../i18n/I18nProvider";
|
|||||||
// One loader for all three sections: same fetch/loading/empty/error shape, so the sections stay
|
// One loader for all three sections: same fetch/loading/empty/error shape, so the sections stay
|
||||||
// consistent and each one is just its own rendering.
|
// consistent and each one is just its own rendering.
|
||||||
function useIntelligence<T>(load: () => Promise<T>, deps: React.DependencyList) {
|
function useIntelligence<T>(load: () => Promise<T>, deps: React.DependencyList) {
|
||||||
|
const { t } = useI18n();
|
||||||
const [data, setData] = useState<T | null>(null);
|
const [data, setData] = useState<T | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -38,7 +39,7 @@ function useIntelligence<T>(load: () => Promise<T>, deps: React.DependencyList)
|
|||||||
setError(null);
|
setError(null);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!cancelled) setError(getApiErrorMessage(err, "Could not load this section."));
|
if (!cancelled) setError(getApiErrorMessage(err, t("intelligenceLoadFailed")));
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!cancelled) setLoading(false);
|
if (!cancelled) setLoading(false);
|
||||||
@@ -46,7 +47,7 @@ function useIntelligence<T>(load: () => Promise<T>, deps: React.DependencyList)
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [run]);
|
}, [run, t]);
|
||||||
|
|
||||||
return { data, error, loading };
|
return { data, error, loading };
|
||||||
}
|
}
|
||||||
@@ -110,7 +111,7 @@ function Bullets({ label, values }: { label: string; values: string[] }) {
|
|||||||
// ---------- Timeline ----------
|
// ---------- Timeline ----------
|
||||||
|
|
||||||
export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
||||||
const { t } = useI18n();
|
const { language, t } = useI18n();
|
||||||
const [category, setCategory] = useState<string>("");
|
const [category, setCategory] = useState<string>("");
|
||||||
const { data, error, loading } = useIntelligence<Timeline>(
|
const { data, error, loading } = useIntelligence<Timeline>(
|
||||||
() => applicationIntelligenceApi.timeline(jobId, category || undefined),
|
() => applicationIntelligenceApi.timeline(jobId, category || undefined),
|
||||||
@@ -129,7 +130,7 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) {
|
|||||||
<Stack key={m.id} direction="row" spacing={1} justifyContent="space-between" alignItems="baseline">
|
<Stack key={m.id} direction="row" spacing={1} justifyContent="space-between" alignItems="baseline">
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{m.summary}</Typography>
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>{m.summary}</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{new Date(m.at).toLocaleDateString()}
|
{new Date(m.at).toLocaleDateString(language === "nb" ? "nb-NO" : "en")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
@@ -267,7 +268,7 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
|
|||||||
<SectionShell
|
<SectionShell
|
||||||
title={t("workspaceCvMatch")}
|
title={t("workspaceCvMatch")}
|
||||||
subtitle={data?.selectedCvName
|
subtitle={data?.selectedCvName
|
||||||
? `${t("workspaceComparingCv")} ${data.selectedCvName}`
|
? t("workspaceComparingCv", { name: data.selectedCvName })
|
||||||
: t("workspaceSelectCvMatch")}
|
: t("workspaceSelectCvMatch")}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
@@ -286,7 +287,7 @@ export function ApplicationMatch({ jobId }: { jobId: number }) {
|
|||||||
<Box>
|
<Box>
|
||||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline">
|
<Stack direction="row" justifyContent="space-between" alignItems="baseline">
|
||||||
<Typography variant="h4" sx={{ fontWeight: 900 }}>{data?.score}%</Typography>
|
<Typography variant="h4" sx={{ fontWeight: 900 }}>{data?.score}%</Typography>
|
||||||
<Chip size="small" label={data?.band} variant="outlined" />
|
<Chip size="small" label={matchBandLabel(t, data?.band)} variant="outlined" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
@@ -369,3 +370,10 @@ function timelineCategoryLabel(t: (key: any) => string, category: string): strin
|
|||||||
};
|
};
|
||||||
return keys[category] ? t(keys[category]) : category;
|
return keys[category] ? t(keys[category]) : category;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchBandLabel(t: (key: any) => string, band?: string): string {
|
||||||
|
if (!band) return t("matchScoreBand_Unknown");
|
||||||
|
return ["Strong", "Partial", "Low", "Unknown"].includes(band)
|
||||||
|
? t(`matchScoreBand_${band}`)
|
||||||
|
: band;
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
|
|||||||
setBoard(await interviewPrepApi.get(jobId));
|
setBoard(await interviewPrepApi.get(jobId));
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not load interview preparation."));
|
setError(getApiErrorMessage(err, t("interviewPrepLoadFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [jobId]);
|
}, [jobId, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
@@ -77,7 +77,7 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
|
|||||||
await run();
|
await run();
|
||||||
await load();
|
await load();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not update interview preparation."));
|
setError(getApiErrorMessage(err, t("interviewPrepUpdateFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
|
|||||||
(board?.groups ?? []).map((group) => (
|
(board?.groups ?? []).map((group) => (
|
||||||
<Box key={group.category}>
|
<Box key={group.category}>
|
||||||
<Typography variant="caption" sx={{ fontWeight: 800, textTransform: "uppercase", letterSpacing: ".06em", color: "text.secondary" }}>
|
<Typography variant="caption" sx={{ fontWeight: 800, textTransform: "uppercase", letterSpacing: ".06em", color: "text.secondary" }}>
|
||||||
{group.label}
|
{interviewCategoryLabel(t, group.category)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack sx={{ mt: 0.5 }}>
|
<Stack sx={{ mt: 0.5 }}>
|
||||||
{group.items.map((item) => (
|
{group.items.map((item) => (
|
||||||
@@ -163,7 +163,7 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) {
|
|||||||
sx={{ minWidth: { sm: 200 } }}
|
sx={{ minWidth: { sm: 200 } }}
|
||||||
>
|
>
|
||||||
{INTERVIEW_PREP_CATEGORIES.map((c) => (
|
{INTERVIEW_PREP_CATEGORIES.map((c) => (
|
||||||
<MenuItem key={c.key} value={c.key}>{c.label}</MenuItem>
|
<MenuItem key={c.key} value={c.key}>{interviewCategoryLabel(t, c.key)}</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -271,8 +271,8 @@ function InterviewAiAssistant({ jobId, onAccepted }: { jobId: number; onAccepted
|
|||||||
{INTERVIEW_AI_FOCUS.map((item) => <MenuItem key={item.key} value={item.key}>{t(item.labelKey)}</MenuItem>)}
|
{INTERVIEW_AI_FOCUS.map((item) => <MenuItem key={item.key} value={item.key}>{t(item.labelKey)}</MenuItem>)}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField select size="small" label={t("coverAiDocumentLanguage")} value={language} onChange={(event) => setLanguage(event.target.value as "en" | "nb-NO")} sx={{ minWidth: 190 }}>
|
<TextField select size="small" label={t("coverAiDocumentLanguage")} value={language} onChange={(event) => setLanguage(event.target.value as "en" | "nb-NO")} sx={{ minWidth: 190 }}>
|
||||||
<MenuItem value="en">English</MenuItem>
|
<MenuItem value="en">{t("languageEnglish")}</MenuItem>
|
||||||
<MenuItem value="nb-NO">Norsk bokmål</MenuItem>
|
<MenuItem value="nb-NO">{t("languageNorwegianBokmal")}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Stack>
|
</Stack>
|
||||||
<TextField multiline minRows={2} fullWidth label={t("coverAiAdditionalInstructions")} value={instructions} onChange={(event) => setInstructions(event.target.value)} />
|
<TextField multiline minRows={2} fullWidth label={t("coverAiAdditionalInstructions")} value={instructions} onChange={(event) => setInstructions(event.target.value)} />
|
||||||
@@ -317,7 +317,7 @@ function PrepRow({ jobId, item, busy, onChanged, onError }: {
|
|||||||
setDraft(null);
|
setDraft(null);
|
||||||
onChanged();
|
onChanged();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
onError(getApiErrorMessage(err, "Could not save this answer."));
|
onError(getApiErrorMessage(err, t("interviewPrepAnswerSaveFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -409,11 +409,11 @@ export function ApplicationFollowUp({ jobId }: { jobId: number }) {
|
|||||||
setAction(result.nextAction ?? "");
|
setAction(result.nextAction ?? "");
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not load follow-up."));
|
setError(getApiErrorMessage(err, t("interviewFollowUpLoadFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [jobId]);
|
}, [jobId, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
@@ -426,7 +426,7 @@ export function ApplicationFollowUp({ jobId }: { jobId: number }) {
|
|||||||
setData(result);
|
setData(result);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getApiErrorMessage(err, "Could not save the follow-up."));
|
setError(getApiErrorMessage(err, t("interviewFollowUpSaveFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -476,3 +476,15 @@ export function ApplicationFollowUp({ jobId }: { jobId: number }) {
|
|||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function interviewCategoryLabel(t: (key: any) => string, category: string) {
|
||||||
|
const keys: Record<string, string> = {
|
||||||
|
"company-research": "interviewCategoryCompanyResearch",
|
||||||
|
technical: "interviewCategoryTechnical",
|
||||||
|
behavioural: "interviewCategoryBehavioural",
|
||||||
|
star: "interviewCategoryStar",
|
||||||
|
question: "interviewCategoryQuestions",
|
||||||
|
note: "interviewCategoryNotes",
|
||||||
|
};
|
||||||
|
return keys[category] ? t(keys[category]) : category;
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export const translations = {
|
|||||||
noWord: "No",
|
noWord: "No",
|
||||||
createAndAddAnother: "Create & add another",
|
createAndAddAnother: "Create & add another",
|
||||||
loading: "Loading...",
|
loading: "Loading...",
|
||||||
|
languageEnglish: "English",
|
||||||
|
languageNorwegianBokmal: "Norsk bokmål",
|
||||||
discoverJobs: "Discover jobs",
|
discoverJobs: "Discover jobs",
|
||||||
discoverJobsSubtitle: "Search official job-board feeds and save opportunities to your tracker.",
|
discoverJobsSubtitle: "Search official job-board feeds and save opportunities to your tracker.",
|
||||||
jobDetails: "Job details",
|
jobDetails: "Job details",
|
||||||
@@ -103,6 +105,15 @@ export const translations = {
|
|||||||
interviewPrepCategory: "Category",
|
interviewPrepCategory: "Category",
|
||||||
interviewPrepAddLabel: "Add a question, topic or note",
|
interviewPrepAddLabel: "Add a question, topic or note",
|
||||||
interviewPrepAdd: "Add",
|
interviewPrepAdd: "Add",
|
||||||
|
interviewPrepLoadFailed: "Could not load interview preparation.",
|
||||||
|
interviewPrepUpdateFailed: "Could not update interview preparation.",
|
||||||
|
interviewPrepAnswerSaveFailed: "Could not save this answer.",
|
||||||
|
interviewCategoryCompanyResearch: "Company research",
|
||||||
|
interviewCategoryTechnical: "Technical preparation",
|
||||||
|
interviewCategoryBehavioural: "Behavioural questions",
|
||||||
|
interviewCategoryStar: "STAR examples",
|
||||||
|
interviewCategoryQuestions: "Questions to ask them",
|
||||||
|
interviewCategoryNotes: "Notes",
|
||||||
interviewAiTitle: "AI interview coach",
|
interviewAiTitle: "AI interview coach",
|
||||||
interviewAiSubtitle: "Uses the linked CV, job advert and application analysis. Suggestions never overwrite your notes.",
|
interviewAiSubtitle: "Uses the linked CV, job advert and application analysis. Suggestions never overwrite your notes.",
|
||||||
interviewAiUsing: "Using",
|
interviewAiUsing: "Using",
|
||||||
@@ -134,7 +145,10 @@ export const translations = {
|
|||||||
interviewFollowUpOpenTasks: "{count} open follow-up tasks on the checklist.",
|
interviewFollowUpOpenTasks: "{count} open follow-up tasks on the checklist.",
|
||||||
interviewFollowUpDate: "Follow up on",
|
interviewFollowUpDate: "Follow up on",
|
||||||
interviewFollowUpEmpty: "No follow-up scheduled. Applications without one can go quiet.",
|
interviewFollowUpEmpty: "No follow-up scheduled. Applications without one can go quiet.",
|
||||||
|
interviewFollowUpLoadFailed: "Could not load follow-up.",
|
||||||
|
interviewFollowUpSaveFailed: "Could not save the follow-up.",
|
||||||
intelligenceMilestones: "Milestones",
|
intelligenceMilestones: "Milestones",
|
||||||
|
intelligenceLoadFailed: "Could not load this section.",
|
||||||
intelligenceTimeline: "Timeline",
|
intelligenceTimeline: "Timeline",
|
||||||
intelligenceTimelineSubtitle: "Everything recorded for this application, newest first.",
|
intelligenceTimelineSubtitle: "Everything recorded for this application, newest first.",
|
||||||
intelligenceTimelineEmpty: "Nothing has happened yet. Activity appears here as the application moves forward.",
|
intelligenceTimelineEmpty: "Nothing has happened yet. Activity appears here as the application moves forward.",
|
||||||
@@ -170,6 +184,10 @@ export const translations = {
|
|||||||
intelligenceRelevantProjects: "Relevant projects",
|
intelligenceRelevantProjects: "Relevant projects",
|
||||||
intelligenceSuggestions: "Suggestions",
|
intelligenceSuggestions: "Suggestions",
|
||||||
assetsCvSubtitle: "The CV variant used for this application. Variants tailor the master career profile without duplicating it.",
|
assetsCvSubtitle: "The CV variant used for this application. Variants tailor the master career profile without duplicating it.",
|
||||||
|
assetsLoadFailed: "Could not load this section.",
|
||||||
|
assetsAttachCvFailed: "Could not change the attached CV.",
|
||||||
|
assetsDuplicateCvFailed: "Could not create a tailored CV copy.",
|
||||||
|
assetsTailoredCopyName: "{name} — tailored copy",
|
||||||
assetsNoCvVariants: "No CV variants yet. Build one in the CV builder; it starts from the master career profile, so your history stays reusable.",
|
assetsNoCvVariants: "No CV variants yet. Build one in the CV builder; it starts from the master career profile, so your history stays reusable.",
|
||||||
assetsAttachedCv: "Attached CV variant",
|
assetsAttachedCv: "Attached CV variant",
|
||||||
assetsAttachedCvHelp: "Changing this only changes which CV the application uses. The CV itself is untouched.",
|
assetsAttachedCvHelp: "Changing this only changes which CV the application uses. The CV itself is untouched.",
|
||||||
@@ -189,6 +207,10 @@ export const translations = {
|
|||||||
assetsCoverPlaceholder: "Write it yourself, start from the template, or generate a tailored draft.",
|
assetsCoverPlaceholder: "Write it yourself, start from the template, or generate a tailored draft.",
|
||||||
assetsDiscardChanges: "Discard changes",
|
assetsDiscardChanges: "Discard changes",
|
||||||
assetsStartTemplate: "Start from template",
|
assetsStartTemplate: "Start from template",
|
||||||
|
assetsCoverSaveFailed: "Could not save the cover letter.",
|
||||||
|
assetsCoverRestoreFailed: "Could not restore that version.",
|
||||||
|
assetsDraftsSaveFailed: "Could not save the application drafts.",
|
||||||
|
coverAiGenerateFailed: "Could not generate a cover-letter suggestion.",
|
||||||
cvEditorBack: "Back to CVs",
|
cvEditorBack: "Back to CVs",
|
||||||
cvEditorName: "CV name",
|
cvEditorName: "CV name",
|
||||||
cvEditorNameRequired: "Enter a name before saving.",
|
cvEditorNameRequired: "Enter a name before saving.",
|
||||||
@@ -1865,6 +1887,8 @@ export const translations = {
|
|||||||
noWord: "Nei",
|
noWord: "Nei",
|
||||||
createAndAddAnother: "Opprett og legg til en til",
|
createAndAddAnother: "Opprett og legg til en til",
|
||||||
loading: "Laster...",
|
loading: "Laster...",
|
||||||
|
languageEnglish: "Engelsk",
|
||||||
|
languageNorwegianBokmal: "Norsk bokmål",
|
||||||
discoverJobs: "Finn jobber",
|
discoverJobs: "Finn jobber",
|
||||||
discoverJobsSubtitle: "Søk i offisielle jobbportaler og lagre muligheter i oversikten din.",
|
discoverJobsSubtitle: "Søk i offisielle jobbportaler og lagre muligheter i oversikten din.",
|
||||||
jobDetails: "Jobbdetaljer",
|
jobDetails: "Jobbdetaljer",
|
||||||
@@ -1889,6 +1913,15 @@ export const translations = {
|
|||||||
interviewPrepCategory: "Kategori",
|
interviewPrepCategory: "Kategori",
|
||||||
interviewPrepAddLabel: "Legg til spørsmål, tema eller notat",
|
interviewPrepAddLabel: "Legg til spørsmål, tema eller notat",
|
||||||
interviewPrepAdd: "Legg til",
|
interviewPrepAdd: "Legg til",
|
||||||
|
interviewPrepLoadFailed: "Kunne ikke laste intervjuforberedelsen.",
|
||||||
|
interviewPrepUpdateFailed: "Kunne ikke oppdatere intervjuforberedelsen.",
|
||||||
|
interviewPrepAnswerSaveFailed: "Kunne ikke lagre dette svaret.",
|
||||||
|
interviewCategoryCompanyResearch: "Selskapsresearch",
|
||||||
|
interviewCategoryTechnical: "Teknisk forberedelse",
|
||||||
|
interviewCategoryBehavioural: "Atferdsbaserte spørsmål",
|
||||||
|
interviewCategoryStar: "STAR-eksempler",
|
||||||
|
interviewCategoryQuestions: "Spørsmål du kan stille",
|
||||||
|
interviewCategoryNotes: "Notater",
|
||||||
interviewAiTitle: "AI-intervjutrening",
|
interviewAiTitle: "AI-intervjutrening",
|
||||||
interviewAiSubtitle: "Bruker tilknyttet CV, stillingsannonse og søknadsanalyse. Forslag overskriver aldri notatene dine.",
|
interviewAiSubtitle: "Bruker tilknyttet CV, stillingsannonse og søknadsanalyse. Forslag overskriver aldri notatene dine.",
|
||||||
interviewAiUsing: "Bruker",
|
interviewAiUsing: "Bruker",
|
||||||
@@ -1920,7 +1953,10 @@ export const translations = {
|
|||||||
interviewFollowUpOpenTasks: "{count} åpne oppfølgingsoppgaver i sjekklisten.",
|
interviewFollowUpOpenTasks: "{count} åpne oppfølgingsoppgaver i sjekklisten.",
|
||||||
interviewFollowUpDate: "Følg opp den",
|
interviewFollowUpDate: "Følg opp den",
|
||||||
interviewFollowUpEmpty: "Ingen oppfølging er planlagt. Søknader uten oppfølging kan bli stille.",
|
interviewFollowUpEmpty: "Ingen oppfølging er planlagt. Søknader uten oppfølging kan bli stille.",
|
||||||
|
interviewFollowUpLoadFailed: "Kunne ikke laste oppfølgingen.",
|
||||||
|
interviewFollowUpSaveFailed: "Kunne ikke lagre oppfølgingen.",
|
||||||
intelligenceMilestones: "Milepæler",
|
intelligenceMilestones: "Milepæler",
|
||||||
|
intelligenceLoadFailed: "Kunne ikke laste denne delen.",
|
||||||
intelligenceTimeline: "Tidslinje",
|
intelligenceTimeline: "Tidslinje",
|
||||||
intelligenceTimelineSubtitle: "Alt som er registrert for søknaden, nyeste først.",
|
intelligenceTimelineSubtitle: "Alt som er registrert for søknaden, nyeste først.",
|
||||||
intelligenceTimelineEmpty: "Ingenting har skjedd ennå. Aktivitet vises her når søknaden går videre.",
|
intelligenceTimelineEmpty: "Ingenting har skjedd ennå. Aktivitet vises her når søknaden går videre.",
|
||||||
@@ -1956,6 +1992,10 @@ export const translations = {
|
|||||||
intelligenceRelevantProjects: "Relevante prosjekter",
|
intelligenceRelevantProjects: "Relevante prosjekter",
|
||||||
intelligenceSuggestions: "Forslag",
|
intelligenceSuggestions: "Forslag",
|
||||||
assetsCvSubtitle: "CV-varianten som brukes for denne søknaden. Varianter tilpasser karriereprofilen uten å duplisere den.",
|
assetsCvSubtitle: "CV-varianten som brukes for denne søknaden. Varianter tilpasser karriereprofilen uten å duplisere den.",
|
||||||
|
assetsLoadFailed: "Kunne ikke laste denne delen.",
|
||||||
|
assetsAttachCvFailed: "Kunne ikke endre den tilknyttede CV-en.",
|
||||||
|
assetsDuplicateCvFailed: "Kunne ikke opprette en tilpasset CV-kopi.",
|
||||||
|
assetsTailoredCopyName: "{name} — tilpasset kopi",
|
||||||
assetsNoCvVariants: "Ingen CV-varianter ennå. Bygg en i CV-byggeren; den starter fra karriereprofilen, slik at historikken kan gjenbrukes.",
|
assetsNoCvVariants: "Ingen CV-varianter ennå. Bygg en i CV-byggeren; den starter fra karriereprofilen, slik at historikken kan gjenbrukes.",
|
||||||
assetsAttachedCv: "Tilknyttet CV-variant",
|
assetsAttachedCv: "Tilknyttet CV-variant",
|
||||||
assetsAttachedCvHelp: "Dette endrer bare hvilken CV søknaden bruker. Selve CV-en forblir urørt.",
|
assetsAttachedCvHelp: "Dette endrer bare hvilken CV søknaden bruker. Selve CV-en forblir urørt.",
|
||||||
@@ -1975,6 +2015,10 @@ export const translations = {
|
|||||||
assetsCoverPlaceholder: "Skriv selv, start fra malen eller generer et tilpasset utkast.",
|
assetsCoverPlaceholder: "Skriv selv, start fra malen eller generer et tilpasset utkast.",
|
||||||
assetsDiscardChanges: "Forkast endringer",
|
assetsDiscardChanges: "Forkast endringer",
|
||||||
assetsStartTemplate: "Start fra mal",
|
assetsStartTemplate: "Start fra mal",
|
||||||
|
assetsCoverSaveFailed: "Kunne ikke lagre søknadsbrevet.",
|
||||||
|
assetsCoverRestoreFailed: "Kunne ikke gjenopprette denne versjonen.",
|
||||||
|
assetsDraftsSaveFailed: "Kunne ikke lagre søknadsutkastene.",
|
||||||
|
coverAiGenerateFailed: "Kunne ikke generere et forslag til søknadsbrev.",
|
||||||
cvEditorBack: "Tilbake til CV-er",
|
cvEditorBack: "Tilbake til CV-er",
|
||||||
cvEditorName: "CV-navn",
|
cvEditorName: "CV-navn",
|
||||||
cvEditorNameRequired: "Skriv inn et navn før du lagrer.",
|
cvEditorNameRequired: "Skriv inn et navn før du lagrer.",
|
||||||
|
|||||||
@@ -57,7 +57,21 @@ function routeGet(overrides: Record<string, any> = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => jest.clearAllMocks());
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
window.localStorage.setItem("uiLanguage", "en");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("localizes interview categories while preserving user-authored preparation", async () => {
|
||||||
|
window.localStorage.setItem("uiLanguage", "nb");
|
||||||
|
routeGet();
|
||||||
|
|
||||||
|
render(<ApplicationInterviewPrep jobId={7} />);
|
||||||
|
|
||||||
|
expect((await screen.findAllByText("Selskapsresearch")).length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("Atferdsbaserte spørsmål").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText("Funding history")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test("prep renders grouped items with progress and marks AI-sourced ones", async () => {
|
test("prep renders grouped items with progress and marks AI-sourced ones", async () => {
|
||||||
routeGet();
|
routeGet();
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ import { PIPELINE_STATUSES, normalizeStatus, statusLabel, statusTone } from "../
|
|||||||
import { useI18n } from "../i18n/I18nProvider";
|
import { useI18n } from "../i18n/I18nProvider";
|
||||||
import { useConfirm } from "../confirm";
|
import { useConfirm } from "../confirm";
|
||||||
import {
|
import {
|
||||||
WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi, workspaceSection,
|
WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi,
|
||||||
|
checklistSystemTranslationToken, workspaceSection,
|
||||||
} from "../applicationWorkspace";
|
} from "../applicationWorkspace";
|
||||||
|
|
||||||
// Phase 5 Milestone 1 — the dedicated Application Workspace.
|
// Phase 5 Milestone 1 — the dedicated Application Workspace.
|
||||||
@@ -315,16 +316,24 @@ function OverviewSection({ overview, onGo, onReload, onEdit }: {
|
|||||||
return <Stack spacing={2}>{[0, 1].map((i) => <Skeleton key={i} variant="rounded" height={120} />)}</Stack>;
|
return <Stack spacing={2}>{[0, 1].map((i) => <Skeleton key={i} variant="rounded" height={120} />)}</Stack>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextStepToken = checklistSystemTranslationToken(overview.nextStep?.key);
|
||||||
|
const nextStepLabel = overview.nextStep
|
||||||
|
? nextStepToken ? t(`checklistItem_${nextStepToken}` as any) : overview.nextStep.label
|
||||||
|
: null;
|
||||||
|
const nextStepReason = overview.nextStep
|
||||||
|
? nextStepToken ? t(`checklistItem_${nextStepToken}Description` as any) : overview.nextStep.reason
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
{overview.nextStep ? (
|
{overview.nextStep ? (
|
||||||
<Paper sx={{ p: 2.5, borderRadius: 3, borderLeft: "4px solid", borderLeftColor: "primary.main" }}>
|
<Paper sx={{ p: 2.5, borderRadius: 3, borderLeft: "4px solid", borderLeftColor: "primary.main" }}>
|
||||||
<Typography variant="overline" color="text.secondary">{t("workspaceNextAction")}</Typography>
|
<Typography variant="overline" color="text.secondary">{t("workspaceNextAction")}</Typography>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 800 }}>{overview.nextStep.label}</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 800 }}>{nextStepLabel}</Typography>
|
||||||
<Typography color="text.secondary" sx={{ mb: 1.5 }}>{overview.nextStep.reason}</Typography>
|
<Typography color="text.secondary" sx={{ mb: 1.5 }}>{nextStepReason}</Typography>
|
||||||
<Button variant="contained" endIcon={<ArrowForwardIcon />}
|
<Button variant="contained" endIcon={<ArrowForwardIcon />}
|
||||||
onClick={() => overview.nextStep?.section && onGo(overview.nextStep.section as WorkspaceSectionKey)}>
|
onClick={() => overview.nextStep?.section && onGo(overview.nextStep.section as WorkspaceSectionKey)}>
|
||||||
{overview.nextStep.label}
|
{nextStepLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user