WIRED WITH FRONTEND
parent
9f6926b1e1
commit
36be406f92
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
)}
|
||||
</div>
|
||||
<div className="ii-pos">{i.position}</div>
|
||||
<div className="ii-meta"><SourceChip item={i} /> <Badge>{i.processing}</Badge></div>
|
||||
<div className="ii-meta">
|
||||
<SourceChip item={i} /> <Badge>{i.processing}</Badge>
|
||||
{i.applicationStatus && i.applicationStatus !== 'CLOSED' && (
|
||||
<Badge>{i.applicationStatus}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', flexShrink: 0 }}>
|
||||
<div className="ii-time">
|
||||
|
|
@ -599,6 +619,9 @@ function ApplicationDetail({ item: i, loading, onPreview, onImport, onParse, onA
|
|||
<div className="ph-role">{i.position}</div>
|
||||
<div className="ph-tags" style={{ marginTop: 8 }}>
|
||||
<SourceChip item={i} /> <Badge>{i.processing}</Badge>{' '}
|
||||
{i.applicationStatus && i.applicationStatus !== 'CLOSED' && (
|
||||
<><Badge>{i.applicationStatus}</Badge>{' '}</>
|
||||
)}
|
||||
<Badge className={i.resumeStatus === 'Parsed' ? 'b-green' : i.resumeStatus === 'Failed' ? 'b-red' : 'b-amber'}>
|
||||
{i.resumeStatus}
|
||||
</Badge>{' '}
|
||||
|
|
|
|||
Loading…
Reference in New Issue