diff --git a/backend/job/candidate/views.py b/backend/job/candidate/views.py index d0cdc73..390b001 100644 --- a/backend/job/candidate/views.py +++ b/backend/job/candidate/views.py @@ -1751,7 +1751,11 @@ class CandidateView: ) async def attach_application_history(self,payloads): - """Stamp is_reapplicant + previous_applications onto list/detail dicts.""" + """Stamp is_reapplicant + previous_applications onto list/detail dicts. + + ``previous_applications`` is every application for that email, including + the open row. ``is_reapplicant`` still means a *different* assigned job. + """ single=not isinstance(payloads,list) records=[payloads] if single else list(payloads or []) emails=[_payload_email(p) for p in records] @@ -1761,15 +1765,15 @@ class CandidateView: continue email=_payload_email(payload) pack=history.get(email) or {"present_in":[],"user":None,"applications":[]} - previous=[] + items=[] + reapplied=False for row in pack.get("applications") or []: - if _is_current_application(row,payload): - continue - if not _is_earlier_application(row,payload): - continue - previous.append(serialize_application_history_item(row)) - previous.sort(key=lambda r: r.get("applied_at") or "",reverse=True) + item=serialize_application_history_item(row) + items.append(item) + if is_assigned_application(item) and not _is_current_application(row,payload): + reapplied=True + items.sort(key=lambda r: r.get("applied_at") or "",reverse=True) payload["present_in"]=list(pack.get("present_in") or []) - payload["is_reapplicant"]=any(is_assigned_application(item) for item in previous) - payload["previous_applications"]=previous + payload["is_reapplicant"]=reapplied + payload["previous_applications"]=items return records[0] if single else records diff --git a/frontend/src/components/ReapplicantHistory.jsx b/frontend/src/components/ReapplicantHistory.jsx index c25cd41..a37d583 100644 --- a/frontend/src/components/ReapplicantHistory.jsx +++ b/frontend/src/components/ReapplicantHistory.jsx @@ -48,26 +48,62 @@ export function applicationStatusLabel(status, item) { return key.charAt(0) + key.slice(1).toLowerCase() } +function historyItemsOf(row) { + if (!row) return [] + if (Array.isArray(row.previousApplications)) return row.previousApplications + if (Array.isArray(row.previous_applications)) return row.previous_applications + return [] +} + +/** Every application for this candidate, including the one currently open. */ +export function candidateApplicationsOf(row) { + if (!row) return [] + const current = currentRowIds(row) + const items = [...historyItemsOf(row)] + if (current.size && !items.some((item) => isSameApplication(item, current))) { + const self = syntheticCurrentApplication(row) + if (self) items.push(self) + } + items.sort((a, b) => { + const ac = isSameApplication(a, current) ? 1 : 0 + const bc = isSameApplication(b, current) ? 1 : 0 + if (ac !== bc) return bc - ac + return (appliedAtMs(b?.applied_at) ?? 0) - (appliedAtMs(a?.applied_at) ?? 0) + }) + return items +} + +/** Applications other than the open row — used by the Reapplied chip. */ export function previousApplicationsOf(row) { if (!row) return [] - const raw = Array.isArray(row.previousApplications) - ? row.previousApplications - : Array.isArray(row.previous_applications) - ? row.previous_applications - : [] const current = currentRowIds(row) - const currentTs = appliedAtMs( - row.received || row.applied_at || row.entry_date || row.when || row.sentAt, - ) - const items = raw.filter((item) => { - if (current.size && isSameApplication(item, current)) return false - const t = appliedAtMs(item?.applied_at) - if (currentTs == null) return true - if (t == null) return false - return t < currentTs - }) - items.sort((a, b) => (appliedAtMs(a?.applied_at) ?? 0) - (appliedAtMs(b?.applied_at) ?? 0)) - return items + return candidateApplicationsOf(row).filter((item) => !isSameApplication(item, current)) +} + +function syntheticCurrentApplication(row) { + const assigned = row.assignedPost || row.assigned_job_post + const position = row.kind !== 'email' && row.position && row.position !== '—' ? row.position : null + const jobTitle = assigned?.title || row.job_title || row.jobTitle || row.currentTitle || position || null + const kind = row.kind + let source = row.source + if (kind === 'form') source = 'form' + else if (kind === 'email') source = 'inbox' + else if (row.manualUploadId || row.manual_upload_candidate_id) source = 'manual' + if (source && String(source).includes('@')) source = 'inbox' + const id = row.id != null && row.id !== '' ? String(row.id) : null + return { + source: source || 'inbox', + inbox_id: row.inboxId || row.inbox_id || null, + message_id: kind === 'email' ? id : (row.message_id || null), + form_data_id: kind === 'form' ? id : (row.form_data_id || null), + manual_upload_candidate_id: row.manualUploadId || row.manual_upload_candidate_id || null, + candidate_id: row.candidate_id || (kind == null && row.jobId ? row.id : null) || null, + user_id: row.userId || row.user_id || null, + job_post_id: row.assignedId || row.jobId || row.job_post_id || null, + job_title: jobTitle, + status: row.applicationStatus || row.processingState || row.status || null, + applied_at: row.received || row.applied || row.applied_at || row.entry_date || row.when || null, + } } function appliedAtMs(value) { @@ -152,7 +188,7 @@ export function hrefForPreviousApplication(item) { if (item.source === 'form' && item.form_data_id) { return `/inbox?open=${encodeURIComponent(item.form_data_id)}&kind=form` } - if (item.source === 'inbox' && item.message_id) { + if ((item.source === 'inbox' || item.source === 'filtered') && item.message_id) { return `/inbox?open=${encodeURIComponent(item.message_id)}&kind=email` } if (item.source === 'manual') { @@ -161,13 +197,22 @@ export function hrefForPreviousApplication(item) { return `/matching?record=${encodeURIComponent(item.manual_upload_candidate_id)}` } } + if (item.user_id) return `/candidate/${encodeURIComponent(item.user_id)}` + if (item.form_data_id) { + return `/inbox?open=${encodeURIComponent(item.form_data_id)}&kind=form` + } + if (item.message_id) { + return `/inbox?open=${encodeURIComponent(item.message_id)}&kind=email` + } return null } -/** Full prior-job list for profile / inbox / add-candidate. */ -export function PreviousApplications({ row, title = 'Previous applications' }) { - const items = previousApplicationsOf(row) +/** Full application list for profile / inbox / add-candidate. */ +export function PreviousApplications({ row, title = 'Total applications' }) { + const items = candidateApplicationsOf(row) if (!items.length) return null + const current = currentRowIds(row) + const heading = title === 'Total applications' ? `Total applications (${items.length})` : title return (