Add attachment selection controls and lazy-load app screens
This commit is contained in:
@@ -79,11 +79,28 @@ namespace JobTrackerApi.Controllers
|
|||||||
|
|
||||||
private sealed record AttachmentContextResult(string Context, List<string> Signals, List<string> UsedFiles);
|
private sealed record AttachmentContextResult(string Context, List<string> Signals, List<string> UsedFiles);
|
||||||
|
|
||||||
private async Task<AttachmentContextResult?> BuildAttachmentContextAsync(int jobId, CancellationToken cancellationToken)
|
private async Task<AttachmentContextResult?> BuildAttachmentContextAsync(int jobId, CancellationToken cancellationToken, string? attachmentIdsCsv = null)
|
||||||
{
|
{
|
||||||
var attachments = await _db.Attachments
|
HashSet<int>? allowedIds = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(attachmentIdsCsv))
|
||||||
|
{
|
||||||
|
allowedIds = attachmentIdsCsv
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(value => int.TryParse(value, out var id) ? id : 0)
|
||||||
|
.Where(id => id > 0)
|
||||||
|
.ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = _db.Attachments
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(a => a.JobApplicationId == jobId)
|
.Where(a => a.JobApplicationId == jobId);
|
||||||
|
|
||||||
|
if (allowedIds is not null && allowedIds.Count > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(a => allowedIds.Contains(a.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var attachments = await query
|
||||||
.OrderByDescending(a => a.UploadDate)
|
.OrderByDescending(a => a.UploadDate)
|
||||||
.Take(4)
|
.Take(4)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
@@ -1754,7 +1771,7 @@ Candidate master CV:
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:int}/generate-application-package")]
|
[HttpPost("{id:int}/generate-application-package")]
|
||||||
public async Task<ActionResult<GenerateApplicationPackageDto>> GenerateApplicationPackage([FromRoute] int id, [FromQuery] string? mode, [FromQuery] string? coverLetterStyle, CancellationToken cancellationToken)
|
public async Task<ActionResult<GenerateApplicationPackageDto>> GenerateApplicationPackage([FromRoute] int id, [FromQuery] string? mode, [FromQuery] string? coverLetterStyle, [FromQuery] string? attachmentIds, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var job = await _db.JobApplications
|
var job = await _db.JobApplications
|
||||||
.Include(j => j.Company)
|
.Include(j => j.Company)
|
||||||
@@ -1780,7 +1797,7 @@ Candidate master CV:
|
|||||||
|
|
||||||
var packageModeInstruction = BuildPackageModeInstruction(mode);
|
var packageModeInstruction = BuildPackageModeInstruction(mode);
|
||||||
var coverLetterStyleInstruction = BuildCoverLetterStyleInstruction(coverLetterStyle);
|
var coverLetterStyleInstruction = BuildCoverLetterStyleInstruction(coverLetterStyle);
|
||||||
var attachmentContext = await BuildAttachmentContextAsync(id, cancellationToken);
|
var attachmentContext = await BuildAttachmentContextAsync(id, cancellationToken, attachmentIds);
|
||||||
|
|
||||||
var packageContext = $@"Job title: {job.JobTitle}
|
var packageContext = $@"Job title: {job.JobTitle}
|
||||||
Company: {job.Company?.Name}
|
Company: {job.Company?.Name}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Last updated: 2026-03-23
|
|||||||
- [ ] Complete final UI overhaul / visual consistency pass
|
- [ ] Complete final UI overhaul / visual consistency pass
|
||||||
- [x] Add frontend 404 page
|
- [x] Add frontend 404 page
|
||||||
- [x] Add frontend route error page
|
- [x] Add frontend route error page
|
||||||
|
- [x] Add route-level lazy loading/code splitting for heavier screens
|
||||||
|
|
||||||
## Job Creation / Company Features
|
## Job Creation / Company Features
|
||||||
- [x] Remove “Next Action” from create job form
|
- [x] Remove “Next Action” from create job form
|
||||||
|
|||||||
+41
-31
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { Suspense, lazy, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { Box, Button, CssBaseline, Typography } from "@mui/material";
|
import { Box, Button, CssBaseline, Typography } from "@mui/material";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
@@ -23,28 +23,30 @@ import { ToastProvider } from "./toast";
|
|||||||
import { ConfirmProvider } from "./confirm";
|
import { ConfirmProvider } from "./confirm";
|
||||||
import { PromptProvider } from "./prompt";
|
import { PromptProvider } from "./prompt";
|
||||||
|
|
||||||
import JobTable, { JobTableColumns } from "./components/JobTable";
|
import JobTable from "./components/JobTable";
|
||||||
import AddJobModal from "./components/AddJobModal";
|
import type { JobTableColumns } from "./components/JobTable";
|
||||||
import KanbanBoard from "./components/KanbanBoard";
|
|
||||||
import DashboardView from "./components/DashboardView";
|
|
||||||
import CompaniesTable from "./components/CompaniesTable";
|
|
||||||
import SettingsView from "./components/SettingsView";
|
|
||||||
import RemindersView from "./components/RemindersView";
|
|
||||||
import QuickCommandDialog from "./components/QuickCommandDialog";
|
|
||||||
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
|
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
|
||||||
import LoginPage from "./pages/LoginPage";
|
import LoginPage from "./pages/LoginPage";
|
||||||
import ProfilePage from "./pages/ProfilePage";
|
|
||||||
import AdminAuditPage from "./pages/AdminAuditPage";
|
|
||||||
import AdminUsersPage from "./pages/AdminUsersPage";
|
|
||||||
import AdminSystemPage from "./pages/AdminSystemPage";
|
|
||||||
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
||||||
import NotFoundPage from "./pages/NotFoundPage";
|
|
||||||
import RouteErrorPage from "./pages/RouteErrorPage";
|
import RouteErrorPage from "./pages/RouteErrorPage";
|
||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
import { clearAuthToken, getAuthToken } from "./auth";
|
import { clearAuthToken, getAuthToken } from "./auth";
|
||||||
import AppShell, { NavItem } from "./layout/AppShell";
|
import AppShell, { NavItem } from "./layout/AppShell";
|
||||||
import { clearAccentColor, getAccentColor, getThemeModePref, setAccentColor, setThemeModePref, ThemeModePref } from "./themePrefs";
|
import { clearAccentColor, getAccentColor, getThemeModePref, setAccentColor, setThemeModePref, ThemeModePref } from "./themePrefs";
|
||||||
|
|
||||||
|
const AddJobModal = lazy(() => import("./components/AddJobModal"));
|
||||||
|
const KanbanBoard = lazy(() => import("./components/KanbanBoard"));
|
||||||
|
const DashboardView = lazy(() => import("./components/DashboardView"));
|
||||||
|
const CompaniesTable = lazy(() => import("./components/CompaniesTable"));
|
||||||
|
const SettingsView = lazy(() => import("./components/SettingsView"));
|
||||||
|
const RemindersView = lazy(() => import("./components/RemindersView"));
|
||||||
|
const QuickCommandDialog = lazy(() => import("./components/QuickCommandDialog"));
|
||||||
|
const ProfilePage = lazy(() => import("./pages/ProfilePage"));
|
||||||
|
const AdminAuditPage = lazy(() => import("./pages/AdminAuditPage"));
|
||||||
|
const AdminUsersPage = lazy(() => import("./pages/AdminUsersPage"));
|
||||||
|
const AdminSystemPage = lazy(() => import("./pages/AdminSystemPage"));
|
||||||
|
const NotFoundPage = lazy(() => import("./pages/NotFoundPage"));
|
||||||
|
|
||||||
type AuthConfig = { requireAuth: boolean };
|
type AuthConfig = { requireAuth: boolean };
|
||||||
type MeResponse = {
|
type MeResponse = {
|
||||||
provider?: "local" | "google" | "external";
|
provider?: "local" | "google" | "external";
|
||||||
@@ -88,6 +90,10 @@ function titleFor(path: string, t: (k: any) => string): string {
|
|||||||
return t("appTitle");
|
return t("appTitle");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PageLoader() {
|
||||||
|
return <Box sx={{ p: 4 }}><Typography variant="h6">Loading...</Typography></Box>;
|
||||||
|
}
|
||||||
|
|
||||||
function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMode, onThemeModeChange, accentColor, onAccentColorChange, onResetAccentColor }: { jobPageSize: 15 | 20 | 25; setJobPageSize: (n: 15 | 20 | 25) => void; jobColumns: JobTableColumns; setJobColumns: (c: JobTableColumns) => void; themeMode: ThemeModePref; onThemeModeChange: (v: ThemeModePref) => void; accentColor: string; onAccentColorChange: (v: string) => void; onResetAccentColor: () => void; }) {
|
function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMode, onThemeModeChange, accentColor, onAccentColorChange, onResetAccentColor }: { jobPageSize: 15 | 20 | 25; setJobPageSize: (n: 15 | 20 | 25) => void; jobColumns: JobTableColumns; setJobColumns: (c: JobTableColumns) => void; themeMode: ThemeModePref; onThemeModeChange: (v: ThemeModePref) => void; accentColor: string; onAccentColorChange: (v: string) => void; onResetAccentColor: () => void; }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -183,25 +189,29 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
|
|||||||
onSignOut={() => { clearAuthToken(); navigate("/login"); }}
|
onSignOut={() => { clearAuthToken(); navigate("/login"); }}
|
||||||
rightActions={rightActions}
|
rightActions={rightActions}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Suspense fallback={<PageLoader />}>
|
||||||
<Route path="/" element={<Navigate to="/jobs" replace />} />
|
<Routes>
|
||||||
<Route path="/dashboard" element={<DashboardView />} />
|
<Route path="/" element={<Navigate to="/jobs" replace />} />
|
||||||
<Route path="/jobs" element={<JobTable refreshToken={refreshToken} pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} mode="jobs" />} />
|
<Route path="/dashboard" element={<DashboardView />} />
|
||||||
<Route path="/reminders" element={<RemindersView />} />
|
<Route path="/jobs" element={<JobTable refreshToken={refreshToken} pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} mode="jobs" />} />
|
||||||
<Route path="/kanban" element={<KanbanBoard />} />
|
<Route path="/reminders" element={<RemindersView />} />
|
||||||
<Route path="/companies" element={<CompaniesTable />} />
|
<Route path="/kanban" element={<KanbanBoard />} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/companies" element={<CompaniesTable />} />
|
||||||
<Route path="/admin/audit" element={<AdminAuditPage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
<Route path="/admin/users" element={<AdminUsersPage />} />
|
<Route path="/admin/audit" element={<AdminAuditPage />} />
|
||||||
<Route path="/admin/system" element={<AdminSystemPage />} />
|
<Route path="/admin/users" element={<AdminUsersPage />} />
|
||||||
<Route path="/trash" element={<JobTable refreshToken={refreshToken} pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} mode="trash" />} />
|
<Route path="/admin/system" element={<AdminSystemPage />} />
|
||||||
<Route path="/settings" element={<SettingsView pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} accentColor={accentColor} onAccentColorChange={onAccentColorChange} onResetAccentColor={onResetAccentColor} />} />
|
<Route path="/trash" element={<JobTable refreshToken={refreshToken} pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} mode="trash" />} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="/settings" element={<SettingsView pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} accentColor={accentColor} onAccentColorChange={onAccentColorChange} onResetAccentColor={onResetAccentColor} />} />
|
||||||
</Routes>
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|
||||||
<AddJobModal open={addOpen} onClose={() => setAddOpen(false)} onCreated={() => { setRefreshToken((t) => t + 1); }} />
|
<Suspense fallback={null}>
|
||||||
<QuickCommandDialog open={quickOpen} onClose={() => setQuickOpen(false)} onNavigate={(to) => navigate(to)} onOpenAddJob={() => setAddOpen(true)} />
|
<AddJobModal open={addOpen} onClose={() => setAddOpen(false)} onCreated={() => { setRefreshToken((t) => t + 1); }} />
|
||||||
|
<QuickCommandDialog open={quickOpen} onClose={() => setQuickOpen(false)} onNavigate={(to) => navigate(to)} onOpenAddJob={() => setAddOpen(true)} />
|
||||||
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ import Attachments from "./Attachments";
|
|||||||
import JobFlowBar from "./JobFlowBar";
|
import JobFlowBar from "./JobFlowBar";
|
||||||
import { useI18n } from "../i18n/I18nProvider";
|
import { useI18n } from "../i18n/I18nProvider";
|
||||||
|
|
||||||
|
type AttachmentItem = {
|
||||||
|
id: number;
|
||||||
|
fileName: string;
|
||||||
|
uploadDate: string;
|
||||||
|
fileType: string;
|
||||||
|
fileSize: number;
|
||||||
|
};
|
||||||
|
|
||||||
type FollowUpDraft = {
|
type FollowUpDraft = {
|
||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
@@ -93,6 +101,8 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
|
|||||||
const [loadingInterviewPrep, setLoadingInterviewPrep] = useState(false);
|
const [loadingInterviewPrep, setLoadingInterviewPrep] = useState(false);
|
||||||
const [readiness, setReadiness] = useState<ReadinessResponse | null>(null);
|
const [readiness, setReadiness] = useState<ReadinessResponse | null>(null);
|
||||||
const [loadingReadiness, setLoadingReadiness] = useState(false);
|
const [loadingReadiness, setLoadingReadiness] = useState(false);
|
||||||
|
const [jobAttachments, setJobAttachments] = useState<AttachmentItem[]>([]);
|
||||||
|
const [selectedAttachmentIds, setSelectedAttachmentIds] = useState<number[]>([]);
|
||||||
const [savingTailoredCv, setSavingTailoredCv] = useState(false);
|
const [savingTailoredCv, setSavingTailoredCv] = useState(false);
|
||||||
const [savingApplicationDrafts, setSavingApplicationDrafts] = useState(false);
|
const [savingApplicationDrafts, setSavingApplicationDrafts] = useState(false);
|
||||||
const [generatingPackage, setGeneratingPackage] = useState(false);
|
const [generatingPackage, setGeneratingPackage] = useState(false);
|
||||||
@@ -115,12 +125,22 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
|
|||||||
setInterviewPrep(null);
|
setInterviewPrep(null);
|
||||||
setReadiness(null);
|
setReadiness(null);
|
||||||
setApplicationPackage(null);
|
setApplicationPackage(null);
|
||||||
|
setJobAttachments([]);
|
||||||
|
setSelectedAttachmentIds([]);
|
||||||
api.get<JobApplication>(`/jobapplications/${jobId}`).then((r) => {
|
api.get<JobApplication>(`/jobapplications/${jobId}`).then((r) => {
|
||||||
setJob(r.data);
|
setJob(r.data);
|
||||||
setTailoredCvText(r.data.tailoredCvText ?? "");
|
setTailoredCvText(r.data.tailoredCvText ?? "");
|
||||||
setDraftRecipient(r.data.company?.recruiterEmail ?? "");
|
setDraftRecipient(r.data.company?.recruiterEmail ?? "");
|
||||||
setFollowUpMode(initialFollowUpMode || (r.data.status?.includes("Interview") ? "post-interview" : r.data.status === "Waiting" ? "waiting-update" : r.data.status === "Offer" ? "offer-checkin" : r.data.status === "Rejected" ? "feedback-request" : "post-apply"));
|
setFollowUpMode(initialFollowUpMode || (r.data.status?.includes("Interview") ? "post-interview" : r.data.status === "Waiting" ? "waiting-update" : r.data.status === "Offer" ? "offer-checkin" : r.data.status === "Rejected" ? "feedback-request" : "post-apply"));
|
||||||
});
|
});
|
||||||
|
api.get<AttachmentItem[]>(`/attachments/${jobId}`).then((r) => {
|
||||||
|
const items = Array.isArray(r.data) ? r.data : [];
|
||||||
|
setJobAttachments(items);
|
||||||
|
setSelectedAttachmentIds(items.slice(0, 3).map((item) => item.id));
|
||||||
|
}).catch(() => {
|
||||||
|
setJobAttachments([]);
|
||||||
|
setSelectedAttachmentIds([]);
|
||||||
|
});
|
||||||
api.get(`/auth/me`).then((r) => setIsAdmin(Boolean(r.data?.roles?.includes("Admin")))).catch(() => setIsAdmin(false));
|
api.get(`/auth/me`).then((r) => setIsAdmin(Boolean(r.data?.roles?.includes("Admin")))).catch(() => setIsAdmin(false));
|
||||||
api.get(`/jobapplications/${jobId}/history`).then((r) => setHistory(r.data)).catch(() => setHistory([]));
|
api.get(`/jobapplications/${jobId}/history`).then((r) => setHistory(r.data)).catch(() => setHistory([]));
|
||||||
}, [open, jobId, initialTab, initialFollowUpMode]);
|
}, [open, jobId, initialTab, initialFollowUpMode]);
|
||||||
@@ -303,7 +323,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
|
|||||||
if (!jobId) return;
|
if (!jobId) return;
|
||||||
setGeneratingPackage(true);
|
setGeneratingPackage(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.post<ApplicationPackageResponse>(`/jobapplications/${jobId}/generate-application-package`, null, { params: { mode: generationMode, coverLetterStyle } });
|
const res = await api.post<ApplicationPackageResponse>(`/jobapplications/${jobId}/generate-application-package`, null, { params: { mode: generationMode, coverLetterStyle, attachmentIds: selectedAttachmentIds.join(",") || undefined } });
|
||||||
setApplicationPackage(res.data);
|
setApplicationPackage(res.data);
|
||||||
setTailoredCvText(res.data.tailoredCvText ?? "");
|
setTailoredCvText(res.data.tailoredCvText ?? "");
|
||||||
toast(t("jobDetailsPackageGenerated"), "success");
|
toast(t("jobDetailsPackageGenerated"), "success");
|
||||||
@@ -333,6 +353,25 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("jobDetailsTailoredCvIntro")}</Typography>
|
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("jobDetailsTailoredCvIntro")}</Typography>
|
||||||
|
{jobAttachments.length > 0 ? (
|
||||||
|
<Box sx={{ mb: 1.5 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mb: 0.75 }}>{t("jobDetailsAttachmentContextPicker")}</Typography>
|
||||||
|
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||||
|
{jobAttachments.map((attachment) => {
|
||||||
|
const selected = selectedAttachmentIds.includes(attachment.id);
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
key={attachment.id}
|
||||||
|
label={attachment.fileName}
|
||||||
|
color={selected ? "primary" : "default"}
|
||||||
|
variant={selected ? "filled" : "outlined"}
|
||||||
|
onClick={() => setSelectedAttachmentIds((current) => current.includes(attachment.id) ? current.filter((id) => id !== attachment.id) : [...current, attachment.id].slice(-4))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
<TextField value={tailoredCvText} onChange={(e) => setTailoredCvText(e.target.value)} multiline minRows={14} fullWidth placeholder={t("jobDetailsTailoredCvPlaceholder")} />
|
<TextField value={tailoredCvText} onChange={(e) => setTailoredCvText(e.target.value)} multiline minRows={14} fullWidth placeholder={t("jobDetailsTailoredCvPlaceholder")} />
|
||||||
<Typography variant="caption" sx={{ color: "text.secondary", mt: 1, display: "block" }}>{t("jobDetailsLastUpdated", { value: job?.tailoredCvUpdatedAt ? new Date(job.tailoredCvUpdatedAt).toLocaleString() : t("jobDetailsNotSavedYet") })}</Typography>
|
<Typography variant="caption" sx={{ color: "text.secondary", mt: 1, display: "block" }}>{t("jobDetailsLastUpdated", { value: job?.tailoredCvUpdatedAt ? new Date(job.tailoredCvUpdatedAt).toLocaleString() : t("jobDetailsNotSavedYet") })}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -693,6 +693,7 @@ export const translations = {
|
|||||||
jobDetailsTailoredCvSaved: "Tailored CV saved.",
|
jobDetailsTailoredCvSaved: "Tailored CV saved.",
|
||||||
jobDetailsTailoredCvSaveFailed: "Failed to save tailored CV.",
|
jobDetailsTailoredCvSaveFailed: "Failed to save tailored CV.",
|
||||||
jobDetailsTailoredCvIntro: "Generate a full application package, then edit and save the tailored resume you actually want to use for this role.",
|
jobDetailsTailoredCvIntro: "Generate a full application package, then edit and save the tailored resume you actually want to use for this role.",
|
||||||
|
jobDetailsAttachmentContextPicker: "Use these attachments as AI context",
|
||||||
jobDetailsTailoredCvPlaceholder: "Paste or rewrite the version of your CV you want to use for this role.",
|
jobDetailsTailoredCvPlaceholder: "Paste or rewrite the version of your CV you want to use for this role.",
|
||||||
jobDetailsLastUpdated: "Last updated: {value}",
|
jobDetailsLastUpdated: "Last updated: {value}",
|
||||||
jobDetailsNotSavedYet: "Not saved yet",
|
jobDetailsNotSavedYet: "Not saved yet",
|
||||||
@@ -1465,6 +1466,7 @@ export const translations = {
|
|||||||
jobDetailsTailoredCvSaved: "Tilpasset CV lagret.",
|
jobDetailsTailoredCvSaved: "Tilpasset CV lagret.",
|
||||||
jobDetailsTailoredCvSaveFailed: "Kunne ikke lagre tilpasset CV.",
|
jobDetailsTailoredCvSaveFailed: "Kunne ikke lagre tilpasset CV.",
|
||||||
jobDetailsTailoredCvIntro: "Generer en full søknadspakke, og rediger og lagre deretter den tilpassede CV-en du faktisk vil bruke for denne rollen.",
|
jobDetailsTailoredCvIntro: "Generer en full søknadspakke, og rediger og lagre deretter den tilpassede CV-en du faktisk vil bruke for denne rollen.",
|
||||||
|
jobDetailsAttachmentContextPicker: "Bruk disse vedleggene som AI-kontekst",
|
||||||
jobDetailsTailoredCvPlaceholder: "Lim inn eller skriv om versjonen av CV-en du vil bruke for denne rollen.",
|
jobDetailsTailoredCvPlaceholder: "Lim inn eller skriv om versjonen av CV-en du vil bruke for denne rollen.",
|
||||||
jobDetailsLastUpdated: "Sist oppdatert: {value}",
|
jobDetailsLastUpdated: "Sist oppdatert: {value}",
|
||||||
jobDetailsNotSavedYet: "Ikke lagret ennå",
|
jobDetailsNotSavedYet: "Ikke lagret ennå",
|
||||||
|
|||||||
Reference in New Issue
Block a user