Add attachment selection controls and lazy-load app screens

This commit is contained in:
cesnimda
2026-03-23 22:23:00 +01:00
parent 0c8258e90f
commit 73983526d3
5 changed files with 106 additions and 37 deletions
@@ -28,6 +28,14 @@ import Attachments from "./Attachments";
import JobFlowBar from "./JobFlowBar";
import { useI18n } from "../i18n/I18nProvider";
type AttachmentItem = {
id: number;
fileName: string;
uploadDate: string;
fileType: string;
fileSize: number;
};
type FollowUpDraft = {
subject: string;
body: string;
@@ -93,6 +101,8 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
const [loadingInterviewPrep, setLoadingInterviewPrep] = useState(false);
const [readiness, setReadiness] = useState<ReadinessResponse | null>(null);
const [loadingReadiness, setLoadingReadiness] = useState(false);
const [jobAttachments, setJobAttachments] = useState<AttachmentItem[]>([]);
const [selectedAttachmentIds, setSelectedAttachmentIds] = useState<number[]>([]);
const [savingTailoredCv, setSavingTailoredCv] = useState(false);
const [savingApplicationDrafts, setSavingApplicationDrafts] = useState(false);
const [generatingPackage, setGeneratingPackage] = useState(false);
@@ -115,12 +125,22 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
setInterviewPrep(null);
setReadiness(null);
setApplicationPackage(null);
setJobAttachments([]);
setSelectedAttachmentIds([]);
api.get<JobApplication>(`/jobapplications/${jobId}`).then((r) => {
setJob(r.data);
setTailoredCvText(r.data.tailoredCvText ?? "");
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"));
});
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(`/jobapplications/${jobId}/history`).then((r) => setHistory(r.data)).catch(() => setHistory([]));
}, [open, jobId, initialTab, initialFollowUpMode]);
@@ -303,7 +323,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
if (!jobId) return;
setGeneratingPackage(true);
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);
setTailoredCvText(res.data.tailoredCvText ?? "");
toast(t("jobDetailsPackageGenerated"), "success");
@@ -333,6 +353,25 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
</Box>
</Box>
<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")} />
<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>