Translate job details and saved views flows
This commit is contained in:
@@ -150,7 +150,7 @@ export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props)
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch {
|
||||
toast("Save failed.", "error");
|
||||
toast(t("editJobSaveFailed"), "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -162,62 +162,62 @@ export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props)
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 1, mb: 2, p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.paper" }}>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||
Update job details, timeline status, documents, and notes from one editing workspace.
|
||||
{t("editJobIntro")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>Application details</Typography>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("editJobApplicationDetails")}</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2, mt: 1 }}>
|
||||
<Autocomplete options={companies} getOptionLabel={(c) => c.name} value={company} onChange={(_, v) => setCompany(v)} renderInput={(params) => <TextField {...params} label="Company" />} />
|
||||
<TextField label="Job title" value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
|
||||
<TextField label="Applied on" type="date" value={dateApplied} onChange={(e) => setDateApplied(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label="Job URL" value={jobUrl} onChange={(e) => setJobUrl(e.target.value)} />
|
||||
<Autocomplete options={companies} getOptionLabel={(c) => c.name} value={company} onChange={(_, v) => setCompany(v)} renderInput={(params) => <TextField {...params} label={t("company")} />} />
|
||||
<TextField label={t("editJobJobTitle")} value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
|
||||
<TextField label={t("editJobAppliedOn")} type="date" value={dateApplied} onChange={(e) => setDateApplied(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label={t("addJobModalJobUrl")} value={jobUrl} onChange={(e) => setJobUrl(e.target.value)} />
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>Status update</Typography>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("editJobStatusUpdate")}</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1fr" }, gap: 2, mt: 1 }}>
|
||||
<TextField select label="Current status" value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
<TextField select label={t("editJobCurrentStatus")} value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
{STATUS_OPTIONS.map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField label="Status changed on" type="date" value={statusChangedAt} onChange={(e) => setStatusChangedAt(e.target.value)} InputLabelProps={{ shrink: true }} helperText={status === initialStatus ? "Only used when you change the status." : "This date will be recorded in the timeline."} />
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}><FormControlLabel control={<Checkbox checked={responseReceived} onChange={(e) => setResponseReceived(e.target.checked)} />} label="Reply received" /></Box>
|
||||
<TextField label="Reply received on" type="date" disabled={!responseReceived} value={responseDate} onChange={(e) => setResponseDate(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label="Next action" value={nextAction} onChange={(e) => setNextAction(e.target.value)} />
|
||||
<TextField label="Follow up on" type="date" value={followUpAt} onChange={(e) => setFollowUpAt(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label={t("editJobStatusChangedOn")} type="date" value={statusChangedAt} onChange={(e) => setStatusChangedAt(e.target.value)} InputLabelProps={{ shrink: true }} helperText={status === initialStatus ? t("editJobStatusChangedHelpIdle") : t("editJobStatusChangedHelpActive")} />
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}><FormControlLabel control={<Checkbox checked={responseReceived} onChange={(e) => setResponseReceived(e.target.checked)} />} label={t("editJobReplyReceived")} /></Box>
|
||||
<TextField label={t("editJobReplyReceivedOn")} type="date" disabled={!responseReceived} value={responseDate} onChange={(e) => setResponseDate(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label={t("editJobNextAction")} value={nextAction} onChange={(e) => setNextAction(e.target.value)} />
|
||||
<TextField label={t("editJobFollowUpOn")} type="date" value={followUpAt} onChange={(e) => setFollowUpAt(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>Role details</Typography>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("editJobRoleDetails")}</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2, mt: 1 }}>
|
||||
<TextField label="Location" value={location} onChange={(e) => setLocation(e.target.value)} />
|
||||
<TextField label="Salary" value={salary} onChange={(e) => setSalary(e.target.value)} />
|
||||
<TextField label="Deadline" type="date" value={deadline} onChange={(e) => setDeadline(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label="Description language" value={descriptionLanguage} onChange={(e) => setDescriptionLanguage(e.target.value)} />
|
||||
<TextField label={t("location")} value={location} onChange={(e) => setLocation(e.target.value)} />
|
||||
<TextField label={t("addJobModalSalary")} value={salary} onChange={(e) => setSalary(e.target.value)} />
|
||||
<TextField label={t("editJobDeadline")} type="date" value={deadline} onChange={(e) => setDeadline(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label={t("editJobDescriptionLanguage")} value={descriptionLanguage} onChange={(e) => setDescriptionLanguage(e.target.value)} />
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><TagsInput value={tags} onChange={setTags} /></Box>
|
||||
<TextField label="Notes" value={notes} onChange={(e) => setNotes(e.target.value)} multiline rows={4} helperText={`${notes.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Description (original)" value={description} onChange={(e) => setDescription(e.target.value)} multiline rows={6} helperText={`${description.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Translated description" value={translatedDescription} onChange={(e) => setTranslatedDescription(e.target.value)} multiline rows={6} helperText={`${translatedDescription.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Cover letter" value={coverLetterText} onChange={(e) => setCoverLetterText(e.target.value)} multiline rows={6} helperText={`${coverLetterText.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label={t("editJobNotes")} value={notes} onChange={(e) => setNotes(e.target.value)} multiline rows={4} helperText={t("correspondenceCharacters", { count: notes.length })} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label={t("editJobDescriptionOriginal")} value={description} onChange={(e) => setDescription(e.target.value)} multiline rows={6} helperText={t("correspondenceCharacters", { count: description.length })} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label={t("editJobTranslatedDescription")} value={translatedDescription} onChange={(e) => setTranslatedDescription(e.target.value)} multiline rows={6} helperText={t("correspondenceCharacters", { count: translatedDescription.length })} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label={t("editJobCoverLetter")} value={coverLetterText} onChange={(e) => setCoverLetterText(e.target.value)} multiline rows={6} helperText={t("correspondenceCharacters", { count: coverLetterText.length })} sx={{ gridColumn: "1 / -1" }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>Attachments checklist</Typography>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("editJobAttachmentsChecklist")}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1, mb: 1.5 }}>
|
||||
<Chip size="small" label={hasResume ? "Resume ready" : "Resume missing"} color={hasResume ? "success" : "default"} variant={hasResume ? "filled" : "outlined"} />
|
||||
<Chip size="small" label={hasCoverLetter ? "Cover letter ready" : "Cover letter missing"} color={hasCoverLetter ? "success" : "default"} variant={hasCoverLetter ? "filled" : "outlined"} />
|
||||
<Chip size="small" label={hasPortfolio ? "Portfolio ready" : "Portfolio optional"} color={hasPortfolio ? "success" : "default"} variant={hasPortfolio ? "filled" : "outlined"} />
|
||||
<Chip size="small" label={hasResume ? t("editJobResumeReady") : t("editJobResumeMissing")} color={hasResume ? "success" : "default"} variant={hasResume ? "filled" : "outlined"} />
|
||||
<Chip size="small" label={hasCoverLetter ? t("editJobCoverLetterReady") : t("editJobCoverLetterMissing")} color={hasCoverLetter ? "success" : "default"} variant={hasCoverLetter ? "filled" : "outlined"} />
|
||||
<Chip size="small" label={hasPortfolio ? t("editJobPortfolioReady") : t("editJobPortfolioOptional")} color={hasPortfolio ? "success" : "default"} variant={hasPortfolio ? "filled" : "outlined"} />
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap", mt: 1 }}>
|
||||
<FormControlLabel control={<Checkbox checked={hasResume} onChange={(e) => setHasResume(e.target.checked)} />} label="Resume" />
|
||||
<FormControlLabel control={<Checkbox checked={hasCoverLetter} onChange={(e) => setHasCoverLetter(e.target.checked)} />} label="Cover letter" />
|
||||
<FormControlLabel control={<Checkbox checked={hasPortfolio} onChange={(e) => setHasPortfolio(e.target.checked)} />} label="Portfolio" />
|
||||
<FormControlLabel control={<Checkbox checked={hasOtherAttachment} onChange={(e) => setHasOtherAttachment(e.target.checked)} />} label="Other attachment" />
|
||||
<FormControlLabel control={<Checkbox checked={hasResume} onChange={(e) => setHasResume(e.target.checked)} />} label={t("editJobResume")} />
|
||||
<FormControlLabel control={<Checkbox checked={hasCoverLetter} onChange={(e) => setHasCoverLetter(e.target.checked)} />} label={t("editJobCoverLetter")} />
|
||||
<FormControlLabel control={<Checkbox checked={hasPortfolio} onChange={(e) => setHasPortfolio(e.target.checked)} />} label={t("editJobPortfolio")} />
|
||||
<FormControlLabel control={<Checkbox checked={hasOtherAttachment} onChange={(e) => setHasOtherAttachment(e.target.checked)} />} label={t("editJobOtherAttachment")} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -155,8 +155,13 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
})();
|
||||
|
||||
const title = job ? `${job.company?.name ?? ""} - ${job.jobTitle}` : t("addJobApplication");
|
||||
const checklist = [job?.hasResume ? "Resume" : null, job?.hasCoverLetter ? "Cover letter" : null, job?.hasPortfolio ? "Portfolio" : null, job?.hasOtherAttachment ? "Other" : null].filter(Boolean).join(", ") || "";
|
||||
const summaryFirstText = job?.fullSummary ?? job?.shortSummary ?? "No summary yet.";
|
||||
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;
|
||||
@@ -182,61 +187,61 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
</Box>
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 2 }} variant="scrollable" allowScrollButtonsMobile>
|
||||
<Tab label={t("jobTableOverview")} />
|
||||
<Tab label="Correspondence" />
|
||||
<Tab label="Attachments" />
|
||||
<Tab label="Tailored CV" />
|
||||
<Tab label={t("jobDetailsTabCorrespondence")} />
|
||||
<Tab label={t("jobDetailsTabAttachments")} />
|
||||
<Tab label={t("jobDetailsTabTailoredCv")} />
|
||||
<Tab label={t("jobTableFollowUp")} />
|
||||
<Tab label="Candidate fit" />
|
||||
<Tab label="Interview prep" />
|
||||
<Tab label={t("jobDetailsTabCandidateFit")} />
|
||||
<Tab label={t("jobDetailsTabInterviewPrep")} />
|
||||
<Tab label={t("jobTableReadiness")} />
|
||||
{isAdmin ? <Tab label="History" /> : null}
|
||||
{isAdmin ? <Tab label={t("jobDetailsTabHistory")} /> : null}
|
||||
</Tabs>
|
||||
|
||||
{tab === 0 && (
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2 }}>
|
||||
<Box><Typography variant="overline">Date Applied</Typography><Typography>{job ? new Date(job.dateApplied).toLocaleDateString() : ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">Days Since</Typography><Typography>{job?.daysSince ?? ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">Location</Typography><Typography>{job?.location ?? ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">Salary</Typography><Typography>{job?.salary ?? ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">Next Action</Typography><Typography>{job?.nextAction ?? ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">Follow Up</Typography><Typography>{job?.followUpAt ? new Date(job.followUpAt).toLocaleDateString() : ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">Deadline</Typography><Typography>{job?.deadline ? new Date(job.deadline).toLocaleDateString() : ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">Tags</Typography><Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 0.5 }}>{tags.length === 0 ? <Typography sx={{ color: "text.secondary" }}>-</Typography> : tags.map((t) => <Chip key={t} label={t} size="small" />)}</Box></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">Attachment Types</Typography><Typography>{checklist}</Typography></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">Job URL</Typography><Typography>{job?.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">{job.jobUrl}</a> : ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsDateApplied")}</Typography><Typography>{job ? new Date(job.dateApplied).toLocaleDateString() : ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsDaysSince")}</Typography><Typography>{job?.daysSince ?? ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobTableLocation")}</Typography><Typography>{job?.location ?? ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsSalary")}</Typography><Typography>{job?.salary ?? ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsNextAction")}</Typography><Typography>{job?.nextAction ?? ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsFollowUp")}</Typography><Typography>{job?.followUpAt ? new Date(job.followUpAt).toLocaleDateString() : ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsDeadline")}</Typography><Typography>{job?.deadline ? new Date(job.deadline).toLocaleDateString() : ""}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsTags")}</Typography><Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 0.5 }}>{tags.length === 0 ? <Typography sx={{ color: "text.secondary" }}>-</Typography> : tags.map((t) => <Chip key={t} label={t} size="small" />)}</Box></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">{t("jobDetailsAttachmentTypes")}</Typography><Typography>{checklist}</Typography></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">{t("jobDetailsJobUrl")}</Typography><Typography>{job?.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">{job.jobUrl}</a> : ""}</Typography></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1", mt: 1 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", flexWrap: "wrap", mb: 0.5 }}>
|
||||
<Typography variant="overline">Summary and skills</Typography>
|
||||
<Typography variant="overline">{t("jobDetailsSummaryAndSkills")}</Typography>
|
||||
<Button size="small" variant="outlined" disabled={refreshingAi} onClick={async () => {
|
||||
if (!jobId) return;
|
||||
if (!(await confirmAction("Overwrite the current summary and skills with a freshly generated version?", { title: "Refresh AI summary", confirmLabel: "Refresh" }))) return;
|
||||
if (!(await confirmAction(t("jobDetailsRefreshAiConfirm"), { title: t("jobDetailsRefreshAiTitle"), confirmLabel: t("jobDetailsRefreshAi") }))) return;
|
||||
setRefreshingAi(true);
|
||||
try {
|
||||
const res = await api.post<JobApplication>(`/jobapplications/${jobId}/refresh-ai`);
|
||||
setJob(res.data);
|
||||
toast("Summary and skills refreshed.", "success");
|
||||
toast(t("jobDetailsSummaryRefreshed"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to refresh summary and skills."), "error");
|
||||
toast(getApiErrorMessage(error, t("jobDetailsSummaryRefreshFailed")), "error");
|
||||
} finally {
|
||||
setRefreshingAi(false);
|
||||
}
|
||||
}}>{refreshingAi ? "Refreshing..." : "Refresh summary and skills"}</Button>
|
||||
}}>{refreshingAi ? t("jobDetailsRefreshing") : t("jobDetailsRefreshAi")}</Button>
|
||||
</Box>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{summaryFirstText}</Typography>
|
||||
</Box>
|
||||
{showTranslatedText ? (
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline">Translated role text</Typography>
|
||||
<Typography variant="overline">{t("jobDetailsTranslatedRoleText")}</Typography>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{translatedDescriptionText}</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
{showOriginalText ? (
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline">Original role text</Typography>
|
||||
<Typography variant="overline">{t("jobDetailsOriginalRoleText")}</Typography>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap", color: "text.secondary" }}>{originalDescriptionText}</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">Notes</Typography><Typography sx={{ whiteSpace: "pre-wrap" }}>{job?.notes ?? ""}</Typography></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">{t("editJobNotes")}</Typography><Typography sx={{ whiteSpace: "pre-wrap" }}>{job?.notes ?? ""}</Typography></Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -247,27 +252,27 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap", mb: 1 }}>
|
||||
<Typography variant="overline">Tailored CV for this role</Typography>
|
||||
<Typography variant="overline">{t("jobDetailsTabTailoredCv")}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||
<InputLabel>Generation mode</InputLabel>
|
||||
<Select value={generationMode} label="Generation mode" onChange={(e) => setGenerationMode(e.target.value as GenerationMode)}>
|
||||
<MenuItem value="default">Balanced</MenuItem>
|
||||
<MenuItem value="concise">Concise</MenuItem>
|
||||
<MenuItem value="ats">ATS focused</MenuItem>
|
||||
<MenuItem value="achievement">Achievement focused</MenuItem>
|
||||
<MenuItem value="interview">Interview focused</MenuItem>
|
||||
<InputLabel>{t("jobDetailsTailoredCvMode")}</InputLabel>
|
||||
<Select value={generationMode} label={t("jobDetailsTailoredCvMode")} onChange={(e) => setGenerationMode(e.target.value as GenerationMode)}>
|
||||
<MenuItem value="default">{t("jobDetailsGenerationDefault")}</MenuItem>
|
||||
<MenuItem value="concise">{t("jobDetailsGenerationConcise")}</MenuItem>
|
||||
<MenuItem value="ats">{t("jobDetailsGenerationAts")}</MenuItem>
|
||||
<MenuItem value="achievement">{t("jobDetailsGenerationAchievement")}</MenuItem>
|
||||
<MenuItem value="interview">{t("jobDetailsGenerationInterview")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button size="small" variant="outlined" onClick={async () => {
|
||||
try {
|
||||
const me = await api.get<{ profileCvText?: string | null }>("/auth/me");
|
||||
setTailoredCvText(me.data?.profileCvText ?? "");
|
||||
toast("Loaded your master CV into the tailored editor.", "success");
|
||||
toast(t("jobDetailsLoadedMasterCv"), "success");
|
||||
} catch {
|
||||
toast("Failed to load your master CV.", "error");
|
||||
toast(t("jobDetailsLoadMasterCvFailed"), "error");
|
||||
}
|
||||
}}>Start from master CV</Button>
|
||||
}}>{t("jobDetailsStartFromMasterCv")}</Button>
|
||||
<Button size="small" variant="outlined" disabled={generatingPackage} onClick={async () => {
|
||||
if (!jobId) return;
|
||||
setGeneratingPackage(true);
|
||||
@@ -275,15 +280,15 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
const res = await api.post<ApplicationPackageResponse>(`/jobapplications/${jobId}/generate-application-package`, null, { params: { mode: generationMode } });
|
||||
setApplicationPackage(res.data);
|
||||
setTailoredCvText(res.data.tailoredCvText ?? "");
|
||||
toast("Application package generated.", "success");
|
||||
toast(t("jobDetailsPackageGenerated"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to generate application package."), "error");
|
||||
toast(getApiErrorMessage(error, t("jobDetailsPackageGenerationFailed")), "error");
|
||||
} finally {
|
||||
setGeneratingPackage(false);
|
||||
}
|
||||
}}>{generatingPackage ? "Generating..." : "Generate application package"}</Button>
|
||||
<Button size="small" variant="outlined" onClick={() => setTailoredCvText("")}>Clear</Button>
|
||||
<Button size="small" variant="outlined" onClick={() => navigator.clipboard.writeText(tailoredCvText)}>Copy</Button>
|
||||
}}>{generatingPackage ? t("jobDetailsGeneratingPackage") : t("jobDetailsGeneratePackage")}</Button>
|
||||
<Button size="small" variant="outlined" onClick={() => setTailoredCvText("")}>{t("jobDetailsClear")}</Button>
|
||||
<Button size="small" variant="outlined" onClick={() => navigator.clipboard.writeText(tailoredCvText)}>{t("jobDetailsCopy")}</Button>
|
||||
<Button size="small" variant="contained" disabled={savingTailoredCv} onClick={async () => {
|
||||
if (!jobId) return;
|
||||
setSavingTailoredCv(true);
|
||||
@@ -292,63 +297,63 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
setJob((prev) => prev ? { ...prev, tailoredCvText, tailoredCvUpdatedAt: new Date().toISOString() } : prev);
|
||||
setReadiness(null);
|
||||
setInterviewPrep(null);
|
||||
toast("Tailored CV saved.", "success");
|
||||
toast(t("jobDetailsTailoredCvSaved"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to save tailored CV."), "error");
|
||||
toast(getApiErrorMessage(error, t("jobDetailsTailoredCvSaveFailed")), "error");
|
||||
} finally {
|
||||
setSavingTailoredCv(false);
|
||||
}
|
||||
}}>{savingTailoredCv ? "Saving..." : "Save tailored CV"}</Button>
|
||||
}}>{savingTailoredCv ? t("jobDetailsSaving") : t("jobDetailsSaveTailoredCv")}</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>Generate a full application package, then edit and save the tailored resume you actually want to use for this role.</Typography>
|
||||
<TextField value={tailoredCvText} onChange={(e) => setTailoredCvText(e.target.value)} multiline minRows={14} fullWidth placeholder="Paste or rewrite the version of your CV you want to use for this role." />
|
||||
<Typography variant="caption" sx={{ color: "text.secondary", mt: 1, display: "block" }}>Last updated: {job?.tailoredCvUpdatedAt ? new Date(job.tailoredCvUpdatedAt).toLocaleString() : "Not saved yet"}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("jobDetailsTailoredCvIntro")}</Typography>
|
||||
<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>
|
||||
</Box>
|
||||
|
||||
{applicationPackage ? (
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
|
||||
<DraftCard title="Cover letter draft" content={applicationPackage.coverLetterDraft ?? "No draft available."} onSave={async (content) => {
|
||||
<DraftCard title={t("jobDetailsCoverLetterDraft")} content={applicationPackage.coverLetterDraft ?? t("jobDetailsNoDraftAvailable")} onSave={async (content) => {
|
||||
if (!jobId) return;
|
||||
setSavingApplicationDrafts(true);
|
||||
try {
|
||||
await api.put(`/jobapplications/${jobId}/application-drafts`, { coverLetterText: content });
|
||||
setJob((prev) => prev ? { ...prev, coverLetterText: content } : prev);
|
||||
setReadiness(null);
|
||||
toast("Cover letter saved to this job.", "success");
|
||||
toast(t("jobDetailsCoverLetterSaved"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to save cover letter."), "error");
|
||||
toast(getApiErrorMessage(error, t("jobDetailsCoverLetterSaveFailed")), "error");
|
||||
} finally {
|
||||
setSavingApplicationDrafts(false);
|
||||
}
|
||||
}} saving={savingApplicationDrafts} />
|
||||
<DraftCard title="Short application answer" content={applicationPackage.applicationAnswerDraft ?? "No draft available."} onSave={async (content) => {
|
||||
<DraftCard title={t("jobDetailsShortApplicationAnswer")} content={applicationPackage.applicationAnswerDraft ?? t("jobDetailsNoDraftAvailable")} onSave={async (content) => {
|
||||
if (!jobId) return;
|
||||
setSavingApplicationDrafts(true);
|
||||
try {
|
||||
await api.put(`/jobapplications/${jobId}/application-drafts`, { notes: `Application answer draft:\n${content}` });
|
||||
setReadiness(null);
|
||||
toast("Application answer saved to notes.", "success");
|
||||
toast(t("jobDetailsApplicationAnswerSaved"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to save application answer."), "error");
|
||||
toast(getApiErrorMessage(error, t("jobDetailsApplicationAnswerSaveFailed")), "error");
|
||||
} finally {
|
||||
setSavingApplicationDrafts(false);
|
||||
}
|
||||
}} saving={savingApplicationDrafts} />
|
||||
<DraftCard title="Recruiter message draft" content={applicationPackage.recruiterMessageDraft ?? "No draft available."} onSave={async (content) => {
|
||||
<DraftCard title={t("jobDetailsRecruiterMessageDraft")} content={applicationPackage.recruiterMessageDraft ?? t("jobDetailsNoDraftAvailable")} onSave={async (content) => {
|
||||
if (!jobId) return;
|
||||
setSavingApplicationDrafts(true);
|
||||
try {
|
||||
await api.put(`/jobapplications/${jobId}/application-drafts`, { recruiterMessageDraft: content });
|
||||
setJob((prev) => prev ? { ...prev, recruiterMessageDraft: content } : prev);
|
||||
toast("Recruiter message saved to this job.", "success");
|
||||
toast(t("jobDetailsRecruiterMessageSaved"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to save recruiter message."), "error");
|
||||
toast(getApiErrorMessage(error, t("jobDetailsRecruiterMessageSaveFailed")), "error");
|
||||
} finally {
|
||||
setSavingApplicationDrafts(false);
|
||||
}
|
||||
}} saving={savingApplicationDrafts} />
|
||||
<ListCard title="Key points to emphasize" items={applicationPackage.keyPoints} />
|
||||
<ListCard title={t("jobDetailsKeyPoints")} items={applicationPackage.keyPoints} />
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
@@ -358,13 +363,13 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
<Box>
|
||||
{loadingDraft ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : followUpDraft ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box><Typography variant="overline">Reason</Typography><Typography>{followUpDraft.reason}</Typography></Box>
|
||||
<Box><Typography variant="overline">Suggested send date</Typography><Typography>{new Date(followUpDraft.suggestedSendOn).toLocaleDateString()}</Typography></Box>
|
||||
<TextField label="Recipient" value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText="Defaults to the company recruiter email when available." />
|
||||
<TextField label="Subject" value={draftSubject} onChange={(e) => setDraftSubject(e.target.value)} />
|
||||
<TextField label="Draft" multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} />
|
||||
<Box><Typography variant="overline">{t("jobDetailsReason")}</Typography><Typography>{followUpDraft.reason}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsSuggestedSendDate")}</Typography><Typography>{new Date(followUpDraft.suggestedSendOn).toLocaleDateString()}</Typography></Box>
|
||||
<TextField label={t("jobDetailsRecipient")} value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText={t("jobDetailsRecipientHelp")} />
|
||||
<TextField label={t("jobDetailsSubject")} value={draftSubject} onChange={(e) => setDraftSubject(e.target.value)} />
|
||||
<TextField label={t("jobDetailsDraft")} multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} />
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button variant="outlined" onClick={() => navigator.clipboard.writeText(`${draftSubject}\n\n${draftBody}`)}>Copy draft</Button>
|
||||
<Button variant="outlined" onClick={() => navigator.clipboard.writeText(`${draftSubject}\n\n${draftBody}`)}>{t("jobDetailsCopyDraft")}</Button>
|
||||
<Button variant="contained" disabled={sendingDraft || !draftSubject.trim() || !draftBody.trim()} onClick={async () => {
|
||||
if (!jobId) return;
|
||||
setSendingDraft(true);
|
||||
@@ -372,16 +377,16 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
await api.post(`/jobapplications/${jobId}/send-followup`, { toEmail: draftRecipient || null, subject: draftSubject, body: draftBody, nextFollowUpAt: followUpDraft.suggestedSendOn || null });
|
||||
setJob((prev) => prev ? { ...prev, followUpAt: followUpDraft.suggestedSendOn } : prev);
|
||||
setReadiness(null);
|
||||
toast("Follow-up sent and logged.", "success");
|
||||
toast(t("jobDetailsFollowUpSent"), "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to send follow-up."), "error");
|
||||
toast(getApiErrorMessage(error, t("jobDetailsFollowUpSendFailed")), "error");
|
||||
} finally {
|
||||
setSendingDraft(false);
|
||||
}
|
||||
}}>{sendingDraft ? "Sending..." : "Send and log email"}</Button>
|
||||
}}>{sendingDraft ? t("jobDetailsSending") : t("jobDetailsSendAndLogEmail")}</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
) : <Typography sx={{ color: "text.secondary" }}>No draft available.</Typography>}
|
||||
) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoDraftAvailable")}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -390,25 +395,25 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
{loadingCandidateFit ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : candidateFit ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2.5 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
<Box><Typography variant="overline">How you match</Typography><Typography sx={{ whiteSpace: "pre-wrap" }}>{candidateFit.matchSummary}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobDetailsHowYouMatch")}</Typography><Typography sx={{ whiteSpace: "pre-wrap" }}>{candidateFit.matchSummary}</Typography></Box>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||
<Chip label={`${candidateFit.matchScore}% match`} color={candidateFit.matchScore >= 75 ? "success" : candidateFit.matchScore >= 55 ? "warning" : "default"} size="small" />
|
||||
<Chip label={t("jobDetailsMatchPercent", { count: candidateFit.matchScore })} color={candidateFit.matchScore >= 75 ? "success" : candidateFit.matchScore >= 55 ? "warning" : "default"} size="small" />
|
||||
{fitLevel ? <Chip label={fitLevel.label} color={fitLevel.color} size="small" /> : null}
|
||||
</Box>
|
||||
</Box>
|
||||
<DraftCard title="Tailored pitch" content={candidateFit.tailoredPitch} />
|
||||
<SectionChips title="Strong matches" items={candidateFit.strengths} color="success" />
|
||||
<SectionChips title="Possible gaps" items={candidateFit.gaps} color="warning" outlined />
|
||||
<TwoColumnSection leftTitle="What to mention" leftItems={candidateFit.mention} rightTitle="What not to overstate" rightItems={candidateFit.avoid} />
|
||||
<TwoColumnSection leftTitle="Improve your CV for this role" leftItems={candidateFit.cvImprovements} rightTitle="Missing keywords to consider" rightItems={candidateFit.missingKeywords} />
|
||||
<TwoColumnSection leftTitle="Interview prep" leftItems={candidateFit.interviewPrep} rightTitle="CV guidance" rightItems={candidateFit.guidance.cv} />
|
||||
<TwoColumnSection leftTitle="Cover letter guidance" leftItems={candidateFit.guidance.coverLetter} rightTitle="Recruiter message guidance" rightItems={candidateFit.guidance.recruiterMessage} />
|
||||
<DraftCard title={t("jobDetailsTailoredPitch")} content={candidateFit.tailoredPitch} />
|
||||
<SectionChips title={t("jobDetailsStrongMatches")} items={candidateFit.strengths} color="success" />
|
||||
<SectionChips title={t("jobDetailsPossibleGaps")} items={candidateFit.gaps} color="warning" outlined />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsWhatToMention")} leftItems={candidateFit.mention} rightTitle={t("jobDetailsWhatNotToOverstate")} rightItems={candidateFit.avoid} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsImproveCv")} leftItems={candidateFit.cvImprovements} rightTitle={t("jobDetailsMissingKeywords")} rightItems={candidateFit.missingKeywords} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsTabInterviewPrep")} leftItems={candidateFit.interviewPrep} rightTitle={t("jobDetailsCvGuidance")} rightItems={candidateFit.guidance.cv} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsCoverLetterGuidance")} leftItems={candidateFit.guidance.coverLetter} rightTitle={t("jobDetailsRecruiterMessageGuidance")} rightItems={candidateFit.guidance.recruiterMessage} />
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
|
||||
<DraftCard title="Cover letter draft" content={candidateFit.coverLetterDraft ?? "No draft available yet."} />
|
||||
<DraftCard title="Recruiter message draft" content={candidateFit.recruiterMessageDraft ?? "No draft available yet."} />
|
||||
<DraftCard title={t("jobDetailsCoverLetterDraft")} content={candidateFit.coverLetterDraft ?? t("jobDetailsNoDraftAvailableYet")} />
|
||||
<DraftCard title={t("jobDetailsRecruiterMessageDraft")} content={candidateFit.recruiterMessageDraft ?? t("jobDetailsNoDraftAvailableYet")} />
|
||||
</Box>
|
||||
</Box>
|
||||
) : <Typography sx={{ color: "text.secondary" }}>Add your profile CV text on the Profile page to generate a candidate fit analysis for this role.</Typography>}
|
||||
) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsCandidateFitEmpty")}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -416,11 +421,11 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
<Box>
|
||||
{loadingInterviewPrep ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : interviewPrep ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<DraftCard title="Interview prep brief" content={interviewPrep.summary} />
|
||||
<TwoColumnSection leftTitle="Talking points" leftItems={interviewPrep.talkingPoints} rightTitle="Likely questions" rightItems={interviewPrep.likelyQuestions} />
|
||||
<ListCard title="Weak spots to prepare for" items={interviewPrep.weakSpots} />
|
||||
<DraftCard title={t("jobDetailsInterviewPrepBrief")} content={interviewPrep.summary} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsTalkingPoints")} leftItems={interviewPrep.talkingPoints} rightTitle={t("jobDetailsLikelyQuestions")} rightItems={interviewPrep.likelyQuestions} />
|
||||
<ListCard title={t("jobDetailsWeakSpots")} items={interviewPrep.weakSpots} />
|
||||
</Box>
|
||||
) : <Typography sx={{ color: "text.secondary" }}>No interview prep available yet.</Typography>}
|
||||
) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoInterviewPrep")}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -429,22 +434,22 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
{loadingReadiness ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : readiness ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
<Typography variant="h6">Application readiness</Typography>
|
||||
<Typography variant="h6">{t("jobDetailsApplicationReadiness")}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Chip label={`${readiness.score}% ready`} color={readiness.score >= 80 ? "success" : readiness.score >= 60 ? "warning" : "default"} />
|
||||
<Chip label={t("jobDetailsReadyPercent", { count: readiness.score })} color={readiness.score >= 80 ? "success" : readiness.score >= 60 ? "warning" : "default"} />
|
||||
<Chip label={readiness.level} variant="outlined" />
|
||||
</Box>
|
||||
</Box>
|
||||
<TwoColumnSection leftTitle="Completed" leftItems={readiness.completed} rightTitle="Still missing" rightItems={readiness.missing} />
|
||||
<ListCard title="Smart reminders" items={readiness.reminders} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsCompleted")} leftItems={readiness.completed} rightTitle={t("jobDetailsStillMissing")} rightItems={readiness.missing} />
|
||||
<ListCard title={t("jobDetailsSmartReminders")} items={readiness.reminders} />
|
||||
</Box>
|
||||
) : <Typography sx={{ color: "text.secondary" }}>No readiness analysis available yet.</Typography>}
|
||||
) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoReadiness")}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tab === 8 && isAdmin && (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{history.length === 0 ? <Typography sx={{ color: "text.secondary" }}>No history yet.</Typography> : history.map((entry) => <PaperRow key={entry.id} type={entry.type} oldValue={entry.oldValue} newValue={entry.newValue} at={entry.at} note={entry.note} />)}
|
||||
{history.length === 0 ? <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoHistory")}</Typography> : history.map((entry) => <PaperRow key={entry.id} type={entry.type} oldValue={entry.oldValue} newValue={entry.newValue} at={entry.at} note={entry.note} />)}
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
@@ -453,14 +458,16 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
}
|
||||
|
||||
function SectionChips({ title, items, color, outlined }: { title: string; items: string[]; color: "success" | "warning"; outlined?: boolean }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
<Typography variant="overline">{title}</Typography>
|
||||
<Button size="small" variant="outlined" onClick={() => copyLines(items)}>Copy</Button>
|
||||
<Button size="small" variant="outlined" onClick={() => copyLines(items)}>{t("jobDetailsCopy")}</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 0.5 }}>
|
||||
{items.length ? items.map((item) => <Chip key={item} label={item} color={color} variant={outlined ? "outlined" : "filled"} size="small" />) : <Typography sx={{ color: "text.secondary" }}>Nothing highlighted yet.</Typography>}
|
||||
{items.length ? items.map((item) => <Chip key={item} label={item} color={color} variant={outlined ? "outlined" : "filled"} size="small" />) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNothingHighlighted")}</Typography>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -476,20 +483,23 @@ function TwoColumnSection({ leftTitle, leftItems, rightTitle, rightItems }: { le
|
||||
}
|
||||
|
||||
function ListCard({ title, items }: { title: string; items: string[] }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap", mb: 1 }}>
|
||||
<Typography variant="overline">{title}</Typography>
|
||||
<Button size="small" variant="outlined" onClick={() => copyLines(items)}>Copy</Button>
|
||||
<Button size="small" variant="outlined" onClick={() => copyLines(items)}>{t("jobDetailsCopy")}</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.75 }}>
|
||||
{items.length ? items.map((item, index) => <Typography key={`${title}-${index}-${item}`} sx={{ color: "text.primary" }}>• {item}</Typography>) : <Typography sx={{ color: "text.secondary" }}>Nothing highlighted yet.</Typography>}
|
||||
{items.length ? items.map((item, index) => <Typography key={`${title}-${index}-${item}`} sx={{ color: "text.primary" }}>• {item}</Typography>) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNothingHighlighted")}</Typography>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function DraftCard({ title, content, onSave, saving }: { title: string; content: string; onSave?: (content: string) => Promise<void> | void; saving?: boolean }) {
|
||||
const { t } = useI18n();
|
||||
const [value, setValue] = React.useState(content);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -501,8 +511,8 @@ function DraftCard({ title, content, onSave, saving }: { title: string; content:
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap", mb: 1 }}>
|
||||
<Typography variant="overline">{title}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button size="small" variant="outlined" onClick={() => navigator.clipboard.writeText(value)}>Copy</Button>
|
||||
{onSave ? <Button size="small" variant="contained" disabled={saving} onClick={() => onSave(value)}>{saving ? "Saving..." : "Save"}</Button> : null}
|
||||
<Button size="small" variant="outlined" onClick={() => navigator.clipboard.writeText(value)}>{t("jobDetailsCopy")}</Button>
|
||||
{onSave ? <Button size="small" variant="contained" disabled={saving} onClick={() => onSave(value)}>{saving ? t("jobDetailsSaving") : t("save")}</Button> : null}
|
||||
</Box>
|
||||
</Box>
|
||||
<TextField value={value} onChange={(e) => setValue(e.target.value)} multiline minRows={6} fullWidth />
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
import BookmarkBorderIcon from "@mui/icons-material/BookmarkBorder";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
export type SavedViewParams = {
|
||||
q?: string;
|
||||
status?: string;
|
||||
@@ -54,6 +56,7 @@ export default function SavedViewsMenu({
|
||||
current: SavedViewParams;
|
||||
onApply: (p: SavedViewParams) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [views, setViews] = useState<SavedView[]>(() => loadViews());
|
||||
@@ -86,7 +89,7 @@ export default function SavedViewsMenu({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Saved views">
|
||||
<Tooltip title={t("savedViewsTooltip")}>
|
||||
<IconButton size="small" onClick={(e) => setAnchor(e.currentTarget)}>
|
||||
<BookmarkBorderIcon fontSize="small" />
|
||||
</IconButton>
|
||||
@@ -100,15 +103,15 @@ export default function SavedViewsMenu({
|
||||
>
|
||||
<MenuItem disableRipple sx={{ cursor: "default", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary="Saved views"
|
||||
secondary="Save the current filters as a 1-click view."
|
||||
primary={t("savedViewsTitle")}
|
||||
secondary={t("savedViewsSubtitle")}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem disableRipple sx={{ cursor: "default" }}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Name"
|
||||
label={t("savedViewsName")}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
fullWidth
|
||||
@@ -116,14 +119,14 @@ export default function SavedViewsMenu({
|
||||
</MenuItem>
|
||||
<MenuItem disableRipple sx={{ cursor: "default", justifyContent: "flex-end" }}>
|
||||
<Button variant="contained" size="small" onClick={add} disabled={!canSave}>
|
||||
Save current
|
||||
{t("savedViewsSaveCurrent")}
|
||||
</Button>
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{!hasAny && (
|
||||
<MenuItem disabled>No saved views yet.</MenuItem>
|
||||
<MenuItem disabled>{t("savedViewsEmpty")}</MenuItem>
|
||||
)}
|
||||
|
||||
{views.map((v) => (
|
||||
@@ -139,12 +142,12 @@ export default function SavedViewsMenu({
|
||||
primary={v.name}
|
||||
secondary={
|
||||
[
|
||||
v.params.status ? `Status: ${v.params.status}` : null,
|
||||
v.params.location ? `Loc: ${v.params.location}` : null,
|
||||
v.params.needsFollowUp ? "Needs follow-up" : null,
|
||||
v.params.status ? t("savedViewsStatusLabel", { value: v.params.status }) : null,
|
||||
v.params.location ? t("savedViewsLocationLabel", { value: v.params.location }) : null,
|
||||
v.params.needsFollowUp ? t("savedViewsNeedsFollowUp") : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ") || "No filters"
|
||||
.join(" · ") || t("savedViewsNoFilters")
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
|
||||
Reference in New Issue
Block a user