Add attachment selection controls and lazy-load app screens
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user