fix(jobs): derive attachment checklist flags from actual Attachments
CI and Deploy / test (pull_request) Successful in 2m4s
CI and Deploy / deploy (pull_request) Has been skipped

Backlog item 4 (Wave 3, first sub-item). HasResume/HasCoverLetter/HasPortfolio/
HasOtherAttachment were manually-editable checkboxes in EditJobDialog,
completely independent of whether a file was actually attached -- classic
drift: mark 'resume ready' by hand, later delete the resume attachment, flag
stays stuck true forever. User confirmed (asked directly, since removing the
manual-override capability is a product decision, not purely technical):
make them fully computed from Attachments, no manual override.

- AttachmentsController.RecomputeAttachmentFlagsAsync: the single place these
  four fields get written now, called after every attachment mutation
  (upload, delete, Purpose change) that could affect them. Deliberately kept
  as persisted columns (not [NotMapped] computed properties reading the
  Attachments navigation collection) -- ~15 query sites build JobApplication
  DTOs without .Include(Attachments), so a live-computed property would
  silently return false everywhere instead of throwing, the worst kind of
  bug. Recomputing at the one write funnel avoids touching any read path.
- Removed HasResume/etc from CreateJobApplicationRequest/
  UpdateJobApplicationRequest -- no longer client-settable.
- EditJobDialog: removed the manual checkboxes, kept the (now genuinely
  accurate) read-only status chips.
- AddJobModal: stopped sending has*-flags at job-creation time; the
  follow-up attachment upload call now sets them correctly via the same
  recompute path.

Caught a real bug while testing this: the Purpose-change path recomputed
before saving the Purpose change, so a fresh query missed the pending edit
and the flags never updated. Fixed by committing the mutation before
recomputing.

3 new backend tests (purpose-change sets flag, delete clears flag,
non-primary purpose counts as "other"). 172/172 backend, 25/25 frontend
suites (57 tests) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-11 21:10:39 +02:00
parent 37ea1f98bb
commit b4fd5e2f96
7 changed files with 165 additions and 44 deletions
@@ -327,10 +327,6 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
notes,
coverLetterText: null,
dateApplied,
hasResume: attachments.resume.length > 0,
hasCoverLetter: attachments.coverLetter.length > 0,
hasPortfolio: attachments.portfolio.length > 0,
hasOtherAttachment: attachments.other.length > 0,
});
if (response.data?.id && attachmentCount > 0) {
@@ -158,10 +158,6 @@ export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props)
salaryPeriod: salaryPeriod || null,
nextAction: nextAction.trim() || null,
followUpAt: followUpAt || null,
hasResume,
hasCoverLetter,
hasPortfolio,
hasOtherAttachment,
notes: notes || null,
description: description || null,
translatedDescription: translatedDescription || null,
@@ -243,16 +239,13 @@ export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props)
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="overline" sx={{ color: "text.secondary" }}>{t("editJobAttachmentsChecklist")}</Typography>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1, mb: 1.5 }}>
{/* Derived from actual uploaded attachments (see the Attachments panel) -- not
manually editable, so this can never drift from what's really attached. */}
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1 }}>
<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={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")} />
{hasOtherAttachment && <Chip size="small" label={t("editJobOtherAttachment")} color="success" variant="filled" />}
</Box>
</Paper>
</Box>