feat: add application draft saving modes and reminder grouping

This commit is contained in:
cesnimda
2026-03-22 18:37:55 +01:00
parent 9188039e9d
commit 8041b43f47
7 changed files with 174 additions and 9 deletions
@@ -8,6 +8,10 @@ import {
Dialog,
DialogContent,
DialogTitle,
FormControl,
InputLabel,
MenuItem,
Select,
Tab,
Tabs,
TextField,
@@ -30,6 +34,8 @@ type FollowUpDraft = {
suggestedSendOn: string;
};
type GenerationMode = "default" | "concise" | "ats" | "achievement" | "interview";
interface Props {
open: boolean;
jobId: number | null;
@@ -81,8 +87,10 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
const [readiness, setReadiness] = useState<ReadinessResponse | null>(null);
const [loadingReadiness, setLoadingReadiness] = useState(false);
const [savingTailoredCv, setSavingTailoredCv] = useState(false);
const [savingApplicationDrafts, setSavingApplicationDrafts] = useState(false);
const [generatingPackage, setGeneratingPackage] = useState(false);
const [applicationPackage, setApplicationPackage] = useState<ApplicationPackageResponse | null>(null);
const [generationMode, setGenerationMode] = useState<GenerationMode>("default");
const [tailoredCvText, setTailoredCvText] = useState("");
const [draftRecipient, setDraftRecipient] = useState("");
const [draftSubject, setDraftSubject] = useState("");
@@ -224,7 +232,17 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
<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>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<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>
</Select>
</FormControl>
<Button size="small" variant="outlined" onClick={async () => {
try {
const me = await api.get<{ profileCvText?: string | null }>("/auth/me");
@@ -238,7 +256,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
if (!jobId) return;
setGeneratingPackage(true);
try {
const res = await api.post<ApplicationPackageResponse>(`/jobapplications/${jobId}/generate-application-package`);
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");
@@ -267,15 +285,40 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
}}>{savingTailoredCv ? "Saving..." : "Save tailored CV"}</Button>
</Box>
</Box>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>Start from your master CV, generate a tailored application package, then edit the resume specifically for this company, role, and interview process.</Typography>
<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>
</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."} />
<DraftCard title="Short application answer" content={applicationPackage.applicationAnswerDraft ?? "No draft available."} />
<DraftCard title="Cover letter draft" content={applicationPackage.coverLetterDraft ?? "No draft available."} 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");
} catch (error: any) {
toast(error?.response?.data || "Failed to save cover letter.", "error");
} finally {
setSavingApplicationDrafts(false);
}
}} saving={savingApplicationDrafts} />
<DraftCard title="Short application answer" content={applicationPackage.applicationAnswerDraft ?? "No draft available."} 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");
} catch (error: any) {
toast(error?.response?.data || "Failed to save application answer.", "error");
} finally {
setSavingApplicationDrafts(false);
}
}} saving={savingApplicationDrafts} />
<DraftCard title="Recruiter message draft" content={applicationPackage.recruiterMessageDraft ?? "No draft available."} />
<ListCard title="Key points to emphasize" items={applicationPackage.keyPoints} />
</Box>
@@ -418,12 +461,15 @@ function ListCard({ title, items }: { title: string; items: string[] }) {
);
}
function DraftCard({ title, content }: { title: string; content: string }) {
function DraftCard({ title, content, onSave, saving }: { title: string; content: string; onSave?: (content: string) => Promise<void> | void; saving?: boolean }) {
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={() => navigator.clipboard.writeText(content)}>Copy</Button>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<Button size="small" variant="outlined" onClick={() => navigator.clipboard.writeText(content)}>Copy</Button>
{onSave ? <Button size="small" variant="contained" disabled={saving} onClick={() => onSave(content)}>{saving ? "Saving..." : "Save"}</Button> : null}
</Box>
</Box>
<Typography sx={{ whiteSpace: "pre-wrap" }}>{content}</Typography>
</Box>