diff --git a/backend/inbox/app.py b/backend/inbox/app.py index 8653268..dce4c6c 100644 --- a/backend/inbox/app.py +++ b/backend/inbox/app.py @@ -126,9 +126,11 @@ async def get_all_applications( ): try: service=Email(session=session) + if application_status == Candidate_application_Status.PROCESS or application_status==Candidate_application_Status.REJECTED: items=await service.get_all_applications(top, skip, search, application_status=application_status) total=await service.count_inbox_messages(search, application_status=application_status) + return JSONResponse(content={"data":items,"total":total,"status_code":200}) if isread==False: items=await service.get_all_applications(top, skip, search, isread=False) total=await service.count_inbox_messages(search, isread=False) diff --git a/backend/inbox/models.py b/backend/inbox/models.py index f0074a4..75b02a6 100644 --- a/backend/inbox/models.py +++ b/backend/inbox/models.py @@ -226,10 +226,12 @@ class Inbox_Messages(SQLModel, table=True): return result.scalars().first() @classmethod - async def count_inbox_messages(cls, session: AsyncSession, search: str | None, isread: bool=True): + async def count_inbox_messages(cls, session: AsyncSession, search: str | None, isread: bool=True, application_status: Candidate_application_Status=Candidate_application_Status.CLOSED): statement = select(func.count()).select_from(cls) if search: statement = statement.where(cls._search_filter(search)) + if application_status == Candidate_application_Status.PROCESS or application_status==Candidate_application_Status.REJECTED: + statement = statement.where(cls.application_status==application_status) if isread==False: statement = statement.where(cls.message_read==False) result = await session.execute(statement) diff --git a/backend/inbox/serializers.py b/backend/inbox/serializers.py index f1c5963..fcab277 100644 --- a/backend/inbox/serializers.py +++ b/backend/inbox/serializers.py @@ -90,6 +90,7 @@ def serialize_application(message: Inbox_Messages) -> dict: "received": message.message_received_time, "unread": not message.message_read, "processing": "Read" if message.message_read else "Unread", + "application_status": message.application_status, "resume_status": _RESUME_STATUS.get(message.match_status, "Pending"), "attachment": _attachment_name(message), "has_attachment": message.attachment, diff --git a/backend/inbox/views.py b/backend/inbox/views.py index 6b6925c..3855cc0 100644 --- a/backend/inbox/views.py +++ b/backend/inbox/views.py @@ -1,7 +1,7 @@ import logging import httpx,os from fastapi import HTTPException -from backend.inbox.enums import Candidate_application_Status +from inbox.enums import Candidate_application_Status from inbox.models import Inbox_Messages from inbox.file_decoder import decode_attachment from inbox.serializers import serialize_application, serialize_message @@ -93,10 +93,9 @@ class Email: return item async def get_all_applications(self,top,skip,search=None,isread:bool=True,application_status:Candidate_application_Status=Candidate_application_Status.CLOSED): - if application_status == Candidate_application_Status.PROCESS or application_status==Candidate_application_Status.REJECTED: messages=await Inbox_Messages.get_inbox_messages(self.session,top,skip,search,application_status=application_status) - if isread==False: + elif isread==False: messages=await Inbox_Messages.get_inbox_messages(self.session,top,skip,search,isread) else: messages=await Inbox_Messages.get_inbox_messages(self.session,top,skip,search) @@ -129,8 +128,10 @@ class Email: task_ids.append(task.task_id) return task_ids - async def count_inbox_messages(self,search=None,isread:bool=True): - if isread==False: + async def count_inbox_messages(self,search=None,isread:bool=True,application_status:Candidate_application_Status=Candidate_application_Status.CLOSED): + if application_status == Candidate_application_Status.PROCESS or application_status==Candidate_application_Status.REJECTED: + return await Inbox_Messages.count_inbox_messages(self.session,search,application_status=application_status) + elif isread==False: return await Inbox_Messages.count_inbox_messages(self.session,search,isread=False) else: return await Inbox_Messages.count_inbox_messages(self.session,search) diff --git a/frontend/src/api/inbox.js b/frontend/src/api/inbox.js index 5450dea..beee5c6 100644 --- a/frontend/src/api/inbox.js +++ b/frontend/src/api/inbox.js @@ -17,12 +17,14 @@ export function listMessages() { * Unlike /inbox/fetch this one IS permissioned server-side * (require_permission(INBOX_VIEW)), so a caller without the tag gets a 403. */ -export function listApplications({ search, top, skip, recordId, isread } = {}) { +export function listApplications({ search, top, skip, recordId, isread, applicationStatus } = {}) { return request('/inbox/all-applications', { // `isread` is tri-valued on the wire: omit it for every tab (server defaults // to true = no filter), send false for the Unread tab only. buildUrl drops // undefined but keeps false, so `isread: undefined` sends no param at all. - params: { search, top, skip, record_id: recordId, isread }, + // Same for `application_status`: omit for every tab (server defaults to + // CLOSED = no filter), send PROCESS / REJECTED for those tabs only. + params: { search, top, skip, record_id: recordId, isread, application_status: applicationStatus }, }) } diff --git a/frontend/src/screens/Inbox.jsx b/frontend/src/screens/Inbox.jsx index b00f68e..89772ea 100644 --- a/frontend/src/screens/Inbox.jsx +++ b/frontend/src/screens/Inbox.jsx @@ -29,6 +29,17 @@ import { const TABS = ['All Applications', 'Unread', 'Imported', 'Processed', 'Rejected', 'Duplicates', 'Email'] +/** + * Server-side filters for the tabs that /inbox/all-applications can narrow. + * Unfiltered tabs (and countsQuery) pass `{}` so the backend defaults apply — + * isread=true and application_status=CLOSED both mean "no filter". + */ +const TAB_FILTERS = { + Unread: { isread: false }, + Processed: { applicationStatus: 'PROCESS' }, + Rejected: { applicationStatus: 'REJECTED' }, +} + /** The prototype computed "time ago" against a fixed 2026-07-09T20:00. */ const NOW = new Date('2026-07-09T20:00') @@ -151,11 +162,12 @@ async function fetchMessageDetail(recordId) { /** * GET /inbox/all-applications -> the shape the application tabs render. * - * READ-ONLY: inbox_messages has no columns for processing state, duplicates, - * recruiter, phone, experience or an ATS score, so those arrive null and every - * mutating action on these tabs is disabled until the endpoints exist. - * `processing` is derived from message_read alone, which is why the Imported / - * Processed / Rejected / Duplicates tabs read empty. + * READ-ONLY: inbox_messages has no columns for duplicates, recruiter, phone, + * experience or an ATS score, so those arrive null and every mutating action + * on these tabs is disabled until the endpoints exist. `processing` is derived + * from message_read alone (Read/Unread). Processed / Rejected tabs filter on + * `application_status` (PROCESS / REJECTED); Imported / Duplicates stay empty + * with no backing columns. */ async function fetchApplications(params) { const res = await inboxApi.listApplications(params) @@ -173,6 +185,7 @@ async function fetchApplications(params) { received: parseDate(row.received), unread: Boolean(row.unread), processing: row.processing || 'Unread', + applicationStatus: row.application_status || null, resumeStatus: row.resume_status || 'Pending', attachment: row.attachment, hasAttachment: Boolean(row.has_attachment), @@ -238,24 +251,25 @@ export default function Inbox() { const [assigning, setAssigning] = useState(null) const [noting, setNoting] = useState(null) - // Only the Unread tab filters server-side; every other tab omits the param and - // the backend's default (true) means "no filter". - const isread = tab === 'Unread' ? false : undefined + // Tabs with a server-side filter pass their params; everything else (and + // countsQuery) passes `{}` so the backend defaults mean "no filter". + const tabFilter = TAB_FILTERS[tab] ?? {} const applicationsQuery = useQuery({ - queryKey: qk.mailbox.applications({ isread }), - queryFn: () => fetchApplications({ isread }), + queryKey: qk.mailbox.applications(tabFilter), + queryFn: () => fetchApplications(tabFilter), enabled: tab !== 'Email', }) /** * The tab badges need whole-table counts, which a server-filtered response * cannot give — and there is no counts endpoint. So the unfiltered set stays - * loaded for them. On every tab except Unread this resolves to the SAME query - * key as the list above, so React Query serves both from one request. + * loaded for them. On every tab without a TAB_FILTERS entry this resolves to + * the SAME query key as the list above, so React Query serves both from one + * request. */ const countsQuery = useQuery({ - queryKey: qk.mailbox.applications({ isread: undefined }), + queryKey: qk.mailbox.applications({}), queryFn: () => fetchApplications({}), enabled: tab !== 'Email', }) @@ -293,14 +307,15 @@ export default function Inbox() { }) const counts = useMemo( - // Counted off the UNFILTERED set — `inbox` is server-filtered on the Unread - // tab, so counting it there would report the unread total for every badge. + // Counted off the UNFILTERED set — `inbox` is server-filtered on Unread / + // Processed / Rejected, so counting it there would report that tab's total + // for every badge. () => ({ 'All Applications': allApplications.length, Unread: allApplications.filter((i) => i.processing === 'Unread').length, Imported: allApplications.filter((i) => i.processing === 'Imported').length, - Processed: allApplications.filter((i) => i.processing === 'Processed').length, - Rejected: allApplications.filter((i) => i.processing === 'Rejected').length, + Processed: allApplications.filter((i) => i.applicationStatus === 'PROCESS').length, + Rejected: allApplications.filter((i) => i.applicationStatus === 'REJECTED').length, Duplicates: allApplications.filter((i) => i.duplicate).length, Email: (emailsQuery.data ?? []).filter((e) => e.unread).length, }), @@ -309,13 +324,13 @@ export default function Inbox() { const list = useMemo(() => { let l = inbox - // Unread is already filtered server-side; re-applying it client-side is what - // makes the optimistic mark-read drop the row from the list immediately - // instead of leaving it until the refetch lands. + // Unread / Processed / Rejected are already filtered server-side; re-applying + // client-side keeps the optimistic mark-read drop-off for Unread, and keeps + // Processed/Rejected coherent if a stale cache briefly holds mixed rows. if (tab === 'Unread') l = l.filter((i) => i.processing === 'Unread') else if (tab === 'Imported') l = l.filter((i) => i.processing === 'Imported') - else if (tab === 'Processed') l = l.filter((i) => i.processing === 'Processed') - else if (tab === 'Rejected') l = l.filter((i) => i.processing === 'Rejected') + else if (tab === 'Processed') l = l.filter((i) => i.applicationStatus === 'PROCESS') + else if (tab === 'Rejected') l = l.filter((i) => i.applicationStatus === 'REJECTED') else if (tab === 'Duplicates') l = l.filter((i) => i.duplicate) if (q) l = l.filter((i) => (i.name + i.position + i.source).toLowerCase().includes(q.toLowerCase())) return l @@ -465,7 +480,12 @@ export default function Inbox() { )}
{i.position}
-
{i.processing}
+
+ {i.processing} + {i.applicationStatus && i.applicationStatus !== 'CLOSED' && ( + {i.applicationStatus} + )} +
@@ -599,6 +619,9 @@ function ApplicationDetail({ item: i, loading, onPreview, onImport, onParse, onA
{i.position}
{i.processing}{' '} + {i.applicationStatus && i.applicationStatus !== 'CLOSED' && ( + <>{i.applicationStatus}{' '} + )} {i.resumeStatus} {' '}