Add attachment metadata and overview strategy snapshot

This commit is contained in:
cesnimda
2026-03-23 22:46:44 +01:00
parent 93f5c9beb7
commit 603f5e8b74
8 changed files with 190 additions and 18 deletions
@@ -34,6 +34,8 @@ type AttachmentItem = {
uploadDate: string;
fileType: string;
fileSize: number;
purpose?: string | null;
useForAi: boolean;
};
type FollowUpDraft = {
@@ -97,6 +99,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
const [focusPlan, setFocusPlan] = useState<FocusPlanResponse | null>(null);
const [loadingCandidateFit, setLoadingCandidateFit] = useState(false);
const [loadingFocusPlan, setLoadingFocusPlan] = useState(false);
const [loadingStrategySnapshot, setLoadingStrategySnapshot] = useState(false);
const [interviewPrep, setInterviewPrep] = useState<InterviewPrepResponse | null>(null);
const [loadingInterviewPrep, setLoadingInterviewPrep] = useState(false);
const [readiness, setReadiness] = useState<ReadinessResponse | null>(null);
@@ -137,7 +140,8 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
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));
const defaultIds = items.filter((item) => item.useForAi !== false).slice(0, 3).map((item) => item.id);
setSelectedAttachmentIds(defaultIds.length > 0 ? defaultIds : items.slice(0, 3).map((item) => item.id));
}).catch(() => {
setJobAttachments([]);
setSelectedAttachmentIds([]);
@@ -273,6 +277,40 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
{tab === 0 && (
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2 }}>
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
<Typography variant="overline">{t("jobDetailsStrategySnapshot")}</Typography>
<Button size="small" variant="outlined" disabled={loadingStrategySnapshot} onClick={async () => {
if (!jobId) return;
setLoadingStrategySnapshot(true);
try {
const [fitRes, focusRes] = await Promise.all([
api.get<CandidateFit>(`/jobapplications/${jobId}/candidate-fit`, { params: { attachmentIds: selectedAttachmentCsv || undefined } }),
api.get<FocusPlanResponse>(`/jobapplications/${jobId}/focus-plan`, { params: { attachmentIds: selectedAttachmentCsv || undefined } }),
]);
setCandidateFit(fitRes.data);
setFocusPlan(focusRes.data);
} catch {
toast(t("jobDetailsStrategySnapshotFailed"), "error");
} finally {
setLoadingStrategySnapshot(false);
}
}}>{loadingStrategySnapshot ? t("jobDetailsRefreshing") : t("jobDetailsGenerateStrategySnapshot")}</Button>
</Box>
{candidateFit || focusPlan ? (
<Box sx={{ gridColumn: "1 / -1", p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center", mb: 1 }}>
{candidateFit ? <Chip size="small" color={candidateFit.matchScore >= 75 ? "success" : candidateFit.matchScore >= 55 ? "warning" : "default"} label={t("jobDetailsMatchPercent", { count: candidateFit.matchScore })} /> : null}
{candidateFit?.fitLevel ? <Chip size="small" variant="outlined" label={candidateFit.fitLevel} /> : null}
</Box>
{focusPlan?.strategicSummary ? <Typography sx={{ whiteSpace: "pre-wrap", mb: 1 }}>{focusPlan.strategicSummary}</Typography> : null}
{candidateFit?.matchSummary ? <Typography sx={{ color: "text.secondary", whiteSpace: "pre-wrap", mb: 1.5 }}>{candidateFit.matchSummary}</Typography> : null}
{focusPlan?.immediatePriorities?.length ? <ListCard title={t("jobDetailsImmediatePriorities")} items={focusPlan.immediatePriorities.slice(0, 3)} /> : null}
</Box>
) : (
<Box sx={{ gridColumn: "1 / -1" }}>
<Typography sx={{ color: "text.secondary" }}>{t("jobDetailsStrategySnapshotEmpty")}</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>