import React, { useEffect, useMemo, useState } from "react"; import { Box, Button, Chip, CircularProgress, Dialog, DialogContent, DialogTitle, FormControl, InputLabel, MenuItem, Select, Tab, Tabs, TextField, Typography, } from "@mui/material"; import { alpha } from "@mui/material/styles"; import { api, getApiErrorMessage } from "../api"; import { ApplicationPackageResponse, CandidateFit, FocusPlanResponse, FollowUpDraft, InterviewPrepResponse, JobApplication, MatchScore, ReadinessResponse, StatusSuggestion, TailoredCvDraft } from "../types"; import { statusLabel } from "../pipeline"; import { useToast } from "../toast"; import { useDialogActions } from "../dialogs"; import { emptyTailoredCvDraft, joinLines, normalizeTailoredCvDraft, splitLines } from "../tailoredCvDraft"; import Correspondence from "./Correspondence"; import Attachments from "./Attachments"; import JobFlowBar from "./JobFlowBar"; import { useI18n } from "../i18n/I18nProvider"; import { useJobWorkspaceBaseData } from "./job-workspace/useJobWorkspaceBaseData"; import { useWorkspaceTabCache } from "./job-workspace/useWorkspaceTabCache"; type GenerationMode = "default" | "concise" | "ats" | "achievement" | "interview"; type CoverLetterStyle = "balanced" | "concise" | "formal" | "bold"; type TailoredCvPreviewResponse = { templateId: string; html: string; suggestedFileName: string; }; interface Props { open: boolean; jobId: number | null; onClose: () => void; initialTab?: number; initialFollowUpMode?: string; } function statusChipColor(status: string): "default" | "primary" | "warning" | "error" | "success" { switch (status) { case "Rejected": return "error"; case "Waiting": case "Ghosted": return "warning"; case "Offer": return "success"; case "Applied": default: return "primary"; } } function getFitLevel(candidateFit: CandidateFit | null): { label: string; color: "success" | "warning" | "default" } | null { if (!candidateFit) return null; if (candidateFit.fitLevel === "Strong match") return { label: candidateFit.fitLevel, color: "success" }; if (candidateFit.fitLevel === "Potential match") return { label: candidateFit.fitLevel, color: "warning" }; return { label: candidateFit.fitLevel, color: "default" }; } function copyLines(items: string[]) { return navigator.clipboard.writeText(items.map((item) => `• ${item}`).join("\n")); } const APPLICATION_ANSWER_START = "<<>>"; const APPLICATION_ANSWER_END = "<<>>"; function upsertApplicationAnswerDraft(notes: string | null | undefined, draft: string) { const trimmedNotes = (notes ?? "").trim(); const trimmedDraft = draft.trim(); const block = trimmedDraft ? `${APPLICATION_ANSWER_START}\n${trimmedDraft}\n${APPLICATION_ANSWER_END}` : ""; if (!trimmedNotes) return block; const markerPattern = new RegExp(`${APPLICATION_ANSWER_START}[\\s\\S]*?${APPLICATION_ANSWER_END}`, "g"); if (markerPattern.test(trimmedNotes)) { return block ? trimmedNotes.replace(markerPattern, block).trim() : trimmedNotes.replace(markerPattern, "").trim(); } const legacyPattern = /(?:\n\n)?Application answer draft:\s*\n[\s\S]*$/i; if (legacyPattern.test(trimmedNotes)) { return block ? trimmedNotes.replace(legacyPattern, `\n\n${block}`).trim() : trimmedNotes.replace(legacyPattern, "").trim(); } return block ? `${trimmedNotes}\n\n${block}` : trimmedNotes; } function getWorkspaceStatus(currentValue: string, savedValue: string) { const current = currentValue.trim(); const saved = savedValue.trim(); if (current && current !== saved) return { label: "Unsaved edits", color: "warning" as const }; if (saved) return { label: "Saved to job", color: "success" as const }; if (current) return { label: "Generated only", color: "default" as const }; return { label: "Empty", color: "default" as const }; } function serializeTailoredDraft(draft: TailoredCvDraft) { const normalized = normalizeTailoredCvDraft(draft); return JSON.stringify({ templateId: normalized.templateId, headline: normalized.headline ?? "", summary: normalized.summary, selectedSkills: normalized.selectedSkills, experience: normalized.experience, education: normalized.education, customSections: normalized.customSections, renderOptions: normalized.renderOptions, status: normalized.status, isLegacyFallback: normalized.isLegacyFallback, }); } export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, initialFollowUpMode }: Props) { const { toast } = useToast(); const { t } = useI18n(); const { confirmAction } = useDialogActions(); const followUpCache = useWorkspaceTabCache(); const candidateFitCache = useWorkspaceTabCache(); const matchScoreCache = useWorkspaceTabCache(); const focusPlanCache = useWorkspaceTabCache(); const interviewPrepCache = useWorkspaceTabCache(); const readinessCache = useWorkspaceTabCache(); const tailoredDraftCache = useWorkspaceTabCache(); const { job, setJob, tab, setTab, history, isAdmin, jobAttachments, selectedAttachmentIds, setSelectedAttachmentIds, profileAvatarImageDataUrl, packageWorkspace, setPackageWorkspace, savedPackageWorkspace, setSavedPackageWorkspace, packageGeneratedAt, setPackageGeneratedAt, draftRecipient, setDraftRecipient, followUpMode, setFollowUpMode, } = useJobWorkspaceBaseData({ open, jobId, initialTab, initialFollowUpMode, }); const [followUpDraft, setFollowUpDraft] = useState(null); const [loadingDraft, setLoadingDraft] = useState(false); const [sendingDraft, setSendingDraft] = useState(false); const [refreshingAi, setRefreshingAi] = useState(false); const [candidateFit, setCandidateFit] = useState(null); const [matchScore, setMatchScore] = useState(null); const [loadingMatchScore, setLoadingMatchScore] = useState(false); const [statusSuggestion, setStatusSuggestion] = useState(null); const [applyingStatusSuggestion, setApplyingStatusSuggestion] = useState(false); const [focusPlan, setFocusPlan] = useState(null); const [loadingCandidateFit, setLoadingCandidateFit] = useState(false); const [loadingFocusPlan, setLoadingFocusPlan] = useState(false); const [loadingStrategySnapshot, setLoadingStrategySnapshot] = useState(false); const [interviewPrep, setInterviewPrep] = useState(null); const [loadingInterviewPrep, setLoadingInterviewPrep] = useState(false); const [readiness, setReadiness] = useState(null); const [loadingReadiness, setLoadingReadiness] = useState(false); const [savingApplicationDrafts, setSavingApplicationDrafts] = useState(false); const [generatingPackage, setGeneratingPackage] = useState(false); const [applicationPackage, setApplicationPackage] = useState(null); const [generationMode, setGenerationMode] = useState("default"); const [coverLetterStyle, setCoverLetterStyle] = useState("balanced"); const [tailoredCvDraft, setTailoredCvDraft] = useState(emptyTailoredCvDraft()); const [savedTailoredCvDraft, setSavedTailoredCvDraft] = useState(emptyTailoredCvDraft()); const [loadingTailoredCvDraft, setLoadingTailoredCvDraft] = useState(false); const [generatingTailoredCvDraft, setGeneratingTailoredCvDraft] = useState(false); const [savingTailoredCvDraft, setSavingTailoredCvDraft] = useState(false); const [tailoredCvPreview, setTailoredCvPreview] = useState(null); const [loadingTailoredCvPreview, setLoadingTailoredCvPreview] = useState(false); const [exportingTailoredCvPdf, setExportingTailoredCvPdf] = useState(false); const [customPhotoDataUrl, setCustomPhotoDataUrl] = useState(null); const [useProfilePhoto, setUseProfilePhoto] = useState(true); const [draftReloadToken, setDraftReloadToken] = useState(0); const [draftSubject, setDraftSubject] = useState(""); const [draftBody, setDraftBody] = useState(""); const selectedAttachmentCsv = useMemo(() => selectedAttachmentIds.join(","), [selectedAttachmentIds]); useEffect(() => { if (!open || !jobId) return; setFollowUpDraft(null); setCandidateFit(null); setMatchScore(null); setStatusSuggestion(null); setFocusPlan(null); setInterviewPrep(null); setReadiness(null); setApplicationPackage(null); setTailoredCvDraft(emptyTailoredCvDraft()); setSavedTailoredCvDraft(emptyTailoredCvDraft()); setTailoredCvPreview(null); setCustomPhotoDataUrl(null); setUseProfilePhoto(true); setDraftReloadToken(0); setDraftSubject(""); setDraftBody(""); followUpCache.clearCached(); candidateFitCache.clearCached(); focusPlanCache.clearCached(); interviewPrepCache.clearCached(); readinessCache.clearCached(); tailoredDraftCache.clearCached(); }, [open, jobId, followUpCache, candidateFitCache, focusPlanCache, interviewPrepCache, readinessCache, tailoredDraftCache]); useEffect(() => { if (!open || !jobId || tab !== 3) return; const cacheKey = `${jobId}:tailored-cv-draft`; const cached = tailoredDraftCache.getCached(cacheKey); if (cached) { const normalized = normalizeTailoredCvDraft(cached); setTailoredCvDraft(normalized); setSavedTailoredCvDraft(normalized); return; } setLoadingTailoredCvDraft(true); api.get(`/jobapplications/${jobId}/tailored-cv-draft`).then((r) => { const normalized = normalizeTailoredCvDraft(r.data); tailoredDraftCache.setCached(cacheKey, normalized); setTailoredCvDraft(normalized); setSavedTailoredCvDraft(normalized); }).catch(() => { const empty = emptyTailoredCvDraft(); setTailoredCvDraft(empty); setSavedTailoredCvDraft(empty); }).finally(() => setLoadingTailoredCvDraft(false)); }, [open, jobId, tab, tailoredDraftCache]); useEffect(() => { if (!open || !jobId || tab !== 4) return; const cacheKey = `${jobId}:followup:${followUpMode}:${selectedAttachmentCsv || "none"}:${draftReloadToken}`; const cached = followUpCache.getCached(cacheKey); if (cached) { setFollowUpDraft(cached); setDraftSubject(cached.subject); setDraftBody(cached.body); return; } setLoadingDraft(true); api.get(`/jobapplications/${jobId}/followup-draft`, { params: { mode: followUpMode, attachmentIds: selectedAttachmentCsv || undefined } }).then((r) => { followUpCache.setCached(cacheKey, r.data); setFollowUpDraft(r.data); setDraftSubject(r.data.subject); setDraftBody(r.data.body); }).catch(() => setFollowUpDraft(null)).finally(() => setLoadingDraft(false)); }, [open, jobId, tab, followUpMode, draftReloadToken, selectedAttachmentCsv, followUpCache]); useEffect(() => { if (!open || !jobId || tab !== 5 || candidateFit) return; const cacheKey = `${jobId}:candidate-fit:${selectedAttachmentCsv || "none"}`; const cached = candidateFitCache.getCached(cacheKey); if (cached) { setCandidateFit(cached); return; } setLoadingCandidateFit(true); api.get(`/jobapplications/${jobId}/candidate-fit`, { params: { attachmentIds: selectedAttachmentCsv || undefined } }).then((r) => { candidateFitCache.setCached(cacheKey, r.data); setCandidateFit(r.data); }).catch(() => setCandidateFit(null)).finally(() => setLoadingCandidateFit(false)); }, [open, jobId, tab, candidateFit, selectedAttachmentCsv, candidateFitCache]); // Match score is deterministic and cheap: load it on the Candidate Fit tab // independently of the slow AI narrative so users see the number instantly. useEffect(() => { if (!open || !jobId || tab !== 5 || matchScore) return; const cacheKey = `${jobId}:match-score`; const cached = matchScoreCache.getCached(cacheKey); if (cached) { setMatchScore(cached); return; } setLoadingMatchScore(true); api.get(`/jobapplications/${jobId}/match-score`).then((r) => { matchScoreCache.setCached(cacheKey, r.data); setMatchScore(r.data); }).catch(() => setMatchScore(null)).finally(() => setLoadingMatchScore(false)); }, [open, jobId, tab, matchScore, matchScoreCache]); // Suggest a status move from the latest inbound email when the workspace opens. useEffect(() => { if (!open || !jobId) return; let cancelled = false; api.get(`/jobapplications/${jobId}/status-suggestion`) .then((r) => { if (!cancelled) setStatusSuggestion(r.data?.hasSuggestion ? r.data : null); }) .catch(() => { if (!cancelled) setStatusSuggestion(null); }); return () => { cancelled = true; }; }, [open, jobId]); const applyStatusSuggestion = async () => { if (!jobId || !statusSuggestion?.suggestedStatus) return; setApplyingStatusSuggestion(true); try { await api.patch(`/jobapplications/${jobId}/status`, { status: statusSuggestion.suggestedStatus }); setJob((prev) => prev ? { ...prev, status: statusSuggestion.suggestedStatus! } : prev); setStatusSuggestion(null); toast(t("statusSuggestionApplied"), "success"); } catch (error: any) { toast(getApiErrorMessage(error, t("statusSuggestionFailed")), "error"); } finally { setApplyingStatusSuggestion(false); } }; useEffect(() => { if (!open || !jobId || tab !== 6 || focusPlan) return; const cacheKey = `${jobId}:focus-plan:${selectedAttachmentCsv || "none"}`; const cached = focusPlanCache.getCached(cacheKey); if (cached) { setFocusPlan(cached); return; } setLoadingFocusPlan(true); api.get(`/jobapplications/${jobId}/focus-plan`, { params: { attachmentIds: selectedAttachmentCsv || undefined } }).then((r) => { focusPlanCache.setCached(cacheKey, r.data); setFocusPlan(r.data); }).catch(() => setFocusPlan(null)).finally(() => setLoadingFocusPlan(false)); }, [open, jobId, tab, focusPlan, selectedAttachmentCsv, focusPlanCache]); useEffect(() => { if (!open || !jobId || tab !== 7 || interviewPrep) return; const cacheKey = `${jobId}:interview-prep:${selectedAttachmentCsv || "none"}`; const cached = interviewPrepCache.getCached(cacheKey); if (cached) { setInterviewPrep(cached); return; } setLoadingInterviewPrep(true); api.get(`/jobapplications/${jobId}/interview-prep`, { params: { attachmentIds: selectedAttachmentCsv || undefined } }).then((r) => { interviewPrepCache.setCached(cacheKey, r.data); setInterviewPrep(r.data); }).catch(() => setInterviewPrep(null)).finally(() => setLoadingInterviewPrep(false)); }, [open, jobId, tab, interviewPrep, selectedAttachmentCsv, interviewPrepCache]); useEffect(() => { setFollowUpDraft(null); setCandidateFit(null); setFocusPlan(null); setInterviewPrep(null); }, [selectedAttachmentCsv]); useEffect(() => { if (!open || !jobId || tab !== 8 || readiness) return; const cacheKey = `${jobId}:readiness`; const cached = readinessCache.getCached(cacheKey); if (cached) { setReadiness(cached); return; } setLoadingReadiness(true); api.get(`/jobapplications/${jobId}/readiness`).then((r) => { readinessCache.setCached(cacheKey, r.data); setReadiness(r.data); }).catch(() => setReadiness(null)).finally(() => setLoadingReadiness(false)); }, [open, jobId, tab, readiness, readinessCache]); const tags: string[] = (() => { const raw = job?.tags; if (!raw) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : []; } catch { return []; } })(); const title = job ? `${job.company?.name ?? ""} - ${job.jobTitle}` : t("addJobApplication"); const checklist = [ job?.hasResume ? t("jobDetailsResume") : null, job?.hasCoverLetter ? t("jobDetailsCoverLetter") : null, job?.hasPortfolio ? t("jobDetailsPortfolio") : null, job?.hasOtherAttachment ? t("jobDetailsOther") : null, ].filter(Boolean).join(", ") || t("jobDetailsNotAvailable"); const summaryFirstText = job?.fullSummary ?? job?.shortSummary ?? t("jobTableNoSummaryYet"); const translatedDescriptionText = job?.translatedDescription?.trim() || ""; const originalDescriptionText = job?.description?.trim() || ""; const showTranslatedText = translatedDescriptionText.length > 0; const showOriginalText = originalDescriptionText.length > 0; const fitLevel = useMemo(() => getFitLevel(candidateFit), [candidateFit]); const showAiAttachmentPicker = tab >= 3 && tab <= 7 && jobAttachments.length > 0; const attachmentPicker = showAiAttachmentPicker ? ( {t("jobDetailsAttachmentContextPicker")} {jobAttachments.map((attachment) => { const selected = selectedAttachmentIds.includes(attachment.id); return ( setSelectedAttachmentIds((current) => current.includes(attachment.id) ? current.filter((id) => id !== attachment.id) : [...current, attachment.id].slice(-4))} /> ); })} ) : null; const tailoredCvDraftStatus = getWorkspaceStatus(tailoredCvDraft.renderedText, savedTailoredCvDraft.renderedText); const coverLetterStatus = getWorkspaceStatus(packageWorkspace.coverLetter, savedPackageWorkspace.coverLetter); const applicationAnswerStatus = getWorkspaceStatus(packageWorkspace.applicationAnswer, savedPackageWorkspace.applicationAnswer); const recruiterMessageStatus = getWorkspaceStatus(packageWorkspace.recruiterMessage, savedPackageWorkspace.recruiterMessage); const hasUnsavedTailoredCvDraftChanges = serializeTailoredDraft(tailoredCvDraft) !== serializeTailoredDraft(savedTailoredCvDraft); const hasUnsavedPackageChanges = [ packageWorkspace.coverLetter.trim() !== savedPackageWorkspace.coverLetter.trim(), packageWorkspace.applicationAnswer.trim() !== savedPackageWorkspace.applicationAnswer.trim(), packageWorkspace.recruiterMessage.trim() !== savedPackageWorkspace.recruiterMessage.trim(), ].some(Boolean); const saveTailoredCvDraft = async () => { if (!jobId) return; const normalized = normalizeTailoredCvDraft({ ...tailoredCvDraft, status: tailoredCvDraft.status === "empty" ? "edited" : tailoredCvDraft.status, }); try { setSavingTailoredCvDraft(true); await api.put(`/jobapplications/${jobId}/tailored-cv-draft`, { templateId: normalized.templateId, headline: normalized.headline, summary: normalized.summary, selectedSkills: normalized.selectedSkills, experience: normalized.experience, education: normalized.education, customSections: normalized.customSections, renderOptions: normalized.renderOptions, status: normalized.status, }); tailoredDraftCache.setCached(`${jobId}:tailored-cv-draft`, normalized); setTailoredCvDraft(normalized); setSavedTailoredCvDraft(normalized); setJob((prev) => prev ? { ...prev, tailoredCvText: normalized.renderedText, tailoredCvUpdatedAt: new Date().toISOString(), } : prev); readinessCache.clearCached(); setReadiness(null); toast("Tailored CV draft saved.", "success"); } catch (error: any) { toast(getApiErrorMessage(error, "Failed to save the tailored CV draft."), "error"); } finally { setSavingTailoredCvDraft(false); } }; const generateTailoredCvDraft = async () => { if (!jobId) return; if (hasUnsavedTailoredCvDraftChanges) { const confirmed = await confirmAction("Regenerating the tailored CV draft will replace your unsaved edits.", { title: "Replace unsaved tailored CV edits?", confirmLabel: "Regenerate draft", }); if (!confirmed) return; } try { setGeneratingTailoredCvDraft(true); const res = await api.post(`/jobapplications/${jobId}/generate-tailored-cv-draft`, null, { params: { mode: generationMode } }); const normalized = normalizeTailoredCvDraft(res.data); tailoredDraftCache.setCached(`${jobId}:tailored-cv-draft`, normalized); setTailoredCvDraft(normalized); setSavedTailoredCvDraft(normalized); setJob((prev) => prev ? { ...prev, tailoredCvText: normalized.renderedText, tailoredCvUpdatedAt: new Date().toISOString(), } : prev); toast("Tailored CV draft generated.", "success"); } catch (error: any) { toast(getApiErrorMessage(error, "Failed to generate a tailored CV draft."), "error"); } finally { setGeneratingTailoredCvDraft(false); } }; const resetTailoredCvDraftToSaved = () => { setTailoredCvDraft(savedTailoredCvDraft); toast("Restored the last saved tailored CV draft.", "info"); }; const buildTailoredCvRenderPayload = () => ({ templateId: tailoredCvDraft.templateId, headline: tailoredCvDraft.headline, summary: tailoredCvDraft.summary, selectedSkills: tailoredCvDraft.selectedSkills, experience: tailoredCvDraft.experience, education: tailoredCvDraft.education, customSections: tailoredCvDraft.customSections, renderOptions: tailoredCvDraft.renderOptions, photoDataUrl: customPhotoDataUrl, useProfileAvatar: useProfilePhoto, }); const refreshTailoredCvPreview = async () => { if (!jobId) return; try { setLoadingTailoredCvPreview(true); const res = await api.post(`/jobapplications/${jobId}/tailored-cv-preview`, buildTailoredCvRenderPayload()); setTailoredCvPreview(res.data); } catch (error: any) { toast(getApiErrorMessage(error, "Failed to build the CV preview."), "error"); } finally { setLoadingTailoredCvPreview(false); } }; const exportTailoredCvPdf = async () => { if (!jobId) return; try { setExportingTailoredCvPdf(true); const response = await api.post(`/jobapplications/${jobId}/export-tailored-cv-pdf`, buildTailoredCvRenderPayload(), { responseType: "blob" }); const blob = new Blob([response.data], { type: "application/pdf" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = tailoredCvPreview?.suggestedFileName || `${(job?.jobTitle ?? "tailored-cv").replace(/\s+/g, "-").toLowerCase()}.pdf`; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); toast("Tailored CV PDF downloaded.", "success"); } catch (error: any) { toast(getApiErrorMessage(error, "Failed to export the CV PDF."), "error"); } finally { setExportingTailoredCvPdf(false); } }; const savePackageWorkspace = async () => { if (!jobId || !job) return; const nextNotes = upsertApplicationAnswerDraft(job.notes, packageWorkspace.applicationAnswer); const draftsChanged = packageWorkspace.coverLetter.trim() !== savedPackageWorkspace.coverLetter.trim() || packageWorkspace.applicationAnswer.trim() !== savedPackageWorkspace.applicationAnswer.trim() || packageWorkspace.recruiterMessage.trim() !== savedPackageWorkspace.recruiterMessage.trim(); if (!draftsChanged) { toast("No unsaved package changes.", "info"); return; } try { setSavingApplicationDrafts(true); await api.put(`/jobapplications/${jobId}/application-drafts`, { coverLetterText: packageWorkspace.coverLetter, notes: nextNotes, recruiterMessageDraft: packageWorkspace.recruiterMessage, }); setJob((prev) => prev ? { ...prev, coverLetterText: packageWorkspace.coverLetter, recruiterMessageDraft: packageWorkspace.recruiterMessage, notes: nextNotes, } : prev); setSavedPackageWorkspace({ ...packageWorkspace }); readinessCache.clearCached(); interviewPrepCache.clearCached(); setReadiness(null); setInterviewPrep(null); toast("Application package saved to this job.", "success"); } catch (error: any) { toast(getApiErrorMessage(error, "Failed to save the application package."), "error"); } finally { setSavingApplicationDrafts(false); } }; const resetPackageWorkspaceToSaved = () => { setPackageWorkspace(savedPackageWorkspace); toast("Restored the last saved package.", "info"); }; return ( {t("jobTableOpen")} {title} {job && } {summaryFirstText} setTab(v)} sx={{ mb: 2 }} variant="scrollable" allowScrollButtonsMobile> {isAdmin ? : null} {attachmentPicker} {statusSuggestion?.hasSuggestion ? ( alpha(theme.palette.warning.main, 0.08), display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1.5, flexWrap: "wrap" }}> {t("statusSuggestionTitle", { status: statusLabel(t, statusSuggestion.suggestedStatus ?? "") })} {t("statusSuggestionReason", { signal: statusSuggestion.signal ?? "", current: statusLabel(t, statusSuggestion.currentStatus ?? "") })} ) : null} {tab === 0 && ( {t("jobDetailsStrategySnapshot")} {candidateFit || focusPlan ? ( {candidateFit ? = 75 ? "success" : candidateFit.matchScore >= 55 ? "warning" : "default"} label={t("jobDetailsMatchPercent", { count: candidateFit.matchScore })} /> : null} {candidateFit?.fitLevel ? : null} {focusPlan?.strategicSummary ? {focusPlan.strategicSummary} : null} {candidateFit?.matchSummary ? {candidateFit.matchSummary} : null} {focusPlan?.immediatePriorities?.length ? : null} ) : ( {t("jobDetailsStrategySnapshotEmpty")} )} {t("jobDetailsDateApplied")}{job ? new Date(job.dateApplied).toLocaleDateString() : ""} {t("jobDetailsDaysSince")}{job?.daysSince ?? ""} {t("jobTableLocation")}{job?.location ?? ""} {t("jobDetailsSalary")}{job?.salary ?? ""} {t("jobDetailsNextAction")}{job?.nextAction ?? ""} {t("jobDetailsFollowUp")}{job?.followUpAt ? new Date(job.followUpAt).toLocaleDateString() : ""} {t("jobDetailsDeadline")}{job?.deadline ? new Date(job.deadline).toLocaleDateString() : ""} {t("jobDetailsTags")}{tags.length === 0 ? - : tags.map((t) => )} {t("jobDetailsAttachmentTypes")}{checklist} {t("jobDetailsJobUrl")}{job?.jobUrl ? {job.jobUrl} : ""} {t("jobDetailsSummaryAndSkills")} {summaryFirstText} {showTranslatedText ? ( {t("jobDetailsTranslatedRoleText")} {translatedDescriptionText} ) : null} {showOriginalText ? ( {t("jobDetailsOriginalRoleText")} {originalDescriptionText} ) : null} {t("editJobNotes")}{job?.notes ?? ""} )} {tab === 1 && jobId && } {tab === 2 && jobId && } {tab === 3 && ( Tailored CV draft This draft is job-scoped. It stays separate from your master CV and from the package drafts below. {t("jobDetailsTailoredCvMode")} Template setTailoredCvDraft((current) => normalizeTailoredCvDraft({ ...current, renderOptions: { ...current.renderOptions, accentColor: e.target.value }, status: "edited", }))} sx={{ width: 110 }} InputLabelProps={{ shrink: true }} /> {customPhotoDataUrl ? : null} {tailoredCvDraft.isLegacyFallback ? : null} {tailoredCvDraft.lastGeneratedAtUtc ? : null} {tailoredCvDraft.canonicalProfileVersion ? : null} {loadingTailoredCvDraft ? ( ) : ( setTailoredCvDraft((current) => normalizeTailoredCvDraft({ ...current, headline: e.target.value, status: "edited" }))} fullWidth /> setTailoredCvDraft((current) => normalizeTailoredCvDraft({ ...current, summary: splitLines(e.target.value), status: "edited" }))} multiline minRows={5} fullWidth helperText="One bullet per line." /> setTailoredCvDraft((current) => normalizeTailoredCvDraft({ ...current, selectedSkills: splitLines(e.target.value), status: "edited" }))} multiline minRows={4} fullWidth helperText="One skill per line." /> [ [item.title, item.company].filter(Boolean).join(" — "), [item.location, item.start, item.end].filter(Boolean).join(" | "), ...(item.bullets ?? []).map((bullet) => `- ${bullet}`), ].filter(Boolean).join("\n")).join("\n\n")} onChange={(e) => setTailoredCvDraft((current) => normalizeTailoredCvDraft({ ...current, experience: e.target.value .split(/\n\s*\n/) .map((block) => block.trim()) .filter(Boolean) .map((block) => { const lines = block.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); const [titleCompany = "", meta = "", ...bulletLines] = lines; const [title = "", company = ""] = titleCompany.split("—").map((part) => part.trim()); const [location = "", start = "", end = ""] = meta.split("|").map((part) => part.trim()); return { title, company, location, start, end, bullets: bulletLines.map((line) => line.replace(/^[-•*]\s*/, "").trim()).filter(Boolean), }; }), status: "edited", }))} multiline minRows={10} fullWidth helperText="Separate entries with a blank line. First line: Title — Company. Second line: Location | Start | End." /> [ [item.qualification, item.institution].filter(Boolean).join(" — "), [item.location, item.start, item.end].filter(Boolean).join(" | "), ...(item.details ?? []).map((detail) => `- ${detail}`), ].filter(Boolean).join("\n")).join("\n\n")} onChange={(e) => setTailoredCvDraft((current) => normalizeTailoredCvDraft({ ...current, education: e.target.value .split(/\n\s*\n/) .map((block) => block.trim()) .filter(Boolean) .map((block) => { const lines = block.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); const [qualificationInstitution = "", meta = "", ...detailLines] = lines; const [qualification = "", institution = ""] = qualificationInstitution.split("—").map((part) => part.trim()); const [location = "", start = "", end = ""] = meta.split("|").map((part) => part.trim()); return { qualification, institution, location, start, end, details: detailLines.map((line) => line.replace(/^[-•*]\s*/, "").trim()).filter(Boolean), }; }), status: "edited", }))} multiline minRows={8} fullWidth helperText="Separate entries with a blank line. First line: Qualification — Institution. Second line: Location | Start | End." /> `${section.title || "Additional Information"}\n${(section.items ?? []).join("\n")}`).join("\n\n")} onChange={(e) => setTailoredCvDraft((current) => normalizeTailoredCvDraft({ ...current, customSections: e.target.value .split(/\n\s*\n/) .map((block) => block.trim()) .filter(Boolean) .map((block) => { const [title = "", ...items] = block.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); return { title, items }; }), status: "edited", }))} multiline minRows={7} fullWidth helperText="Each block starts with the section title, followed by one item per line." /> Rendered CV snapshot This plain-text snapshot stays deterministic and is what the job stores immediately after saving the draft. {t("jobDetailsLastUpdated", { value: job?.tailoredCvUpdatedAt ? new Date(job.tailoredCvUpdatedAt).toLocaleString() : t("jobDetailsNotSavedYet") })} PDF-style preview Preview and PDF export use the same HTML template contract. Accent color and photo settings apply here. {tailoredCvPreview ? (