From 31742c2c490c74cdaa693dfde7eb0424e7c46c92 Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Mon, 31 Aug 2026 12:42:00 +0500 Subject: [PATCH 1/2] correct sort implemented --- backend/inbox/models.py | 17 +---------------- frontend/src/screens/Inbox.jsx | 30 ------------------------------ 2 files changed, 1 insertion(+), 46 deletions(-) diff --git a/backend/inbox/models.py b/backend/inbox/models.py index e65f1eb..ba25ea5 100644 --- a/backend/inbox/models.py +++ b/backend/inbox/models.py @@ -822,7 +822,7 @@ class Inbox_Messages(SQLModel, table=True): # (page-1)*top so page 1 of 25 -> 0..24, page 2 -> 25..49. Newest first # via created_at. statement = cls._apply_filters( - select(cls).order_by(cls.created_at.desc()), + select(cls).order_by(cls.created_at.desc(),cls.id.desc()), search, isread, application_status, assigned, is_duplicate, no_suggestions, processing_state, ) @@ -1213,22 +1213,7 @@ class Inbox_Messages(SQLModel, table=True): class Inbox_Message_Triage(SQLModel, table=True): - """One intake verdict per upstream message id — the gate before inbox_messages. - Rows land here for BOTH outcomes. Rejections are the point: inbox_messages stays - application-only, and a repeated /email/fetch never re-pays for the same - classification. Acceptances are recorded too, so a round that classified and then - failed to insert does not pay twice either. - - Deliberately no message_body column: the body is what this feature keeps out of the - database, and the override route re-reads the mail from upstream by message_id. - message_subject is kept (capped in inbox_classifier.decorators.triage_fields) - because a review screen without it is unusable — it is stored, never logged. - - server_default is load-bearing on every NOT NULL column: alembic_setup runs with - compare_server_default=True, so a model default without a matching server default - autogenerates a drift revision on every boot. - """ __tablename__ = "inbox_message_triage" diff --git a/frontend/src/screens/Inbox.jsx b/frontend/src/screens/Inbox.jsx index 270277a..06bffb9 100644 --- a/frontend/src/screens/Inbox.jsx +++ b/frontend/src/screens/Inbox.jsx @@ -945,24 +945,6 @@ export default function Inbox() { markDuplicate.mutate({ id: item.id, isDuplicate: !item.duplicate, kind: item.kind }) } - const sync = useMutation({ - mutationFn: () => inboxApi.syncMailbox(), - onSuccess: async (res) => { - await qc.invalidateQueries({ queryKey: qk.mailbox.all() }) - // /email/fetch reports what the intake gate did with the page. The key is - // additive and absent when the gate is disabled, so fall back to the old - // message rather than rendering "undefined filtered out". - const t = res?.triage - toast( - t - ? `Mailbox synced — ${t.ingested} imported, ${t.skipped} filtered out` - : 'Mailbox synced', - 'success', - ) - }, - onError: (err) => toast(friendlyAuthError(err, 'Sync failed'), 'error'), - }) - return (
Google Sheets · Form data )} - {!isForms && ( - - )} -- 2.40.1 From 56f529afa175d62edd7202c340372fc2c5669b31 Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Mon, 31 Aug 2026 12:54:24 +0500 Subject: [PATCH 2/2] frontend skip logic done --- backend/inbox/models.py | 4 +--- frontend/src/screens/Inbox.jsx | 44 ++++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/backend/inbox/models.py b/backend/inbox/models.py index ba25ea5..15ab605 100644 --- a/backend/inbox/models.py +++ b/backend/inbox/models.py @@ -818,9 +818,7 @@ class Inbox_Messages(SQLModel, table=True): async def get_inbox_messages( cls, session: AsyncSession, top: int | None, skip: int, search: str | None, isread: bool=True, application_status: Candidate_application_Status=Candidate_application_Status.CLOSED, assigned: bool | None=None, is_duplicate: bool | None=None, no_suggestions: bool | None=None, processing_state: str | None=None ): - # Page size is the caller's `top` (Inbox sends 10/25/50/100); `skip` is - # (page-1)*top so page 1 of 25 -> 0..24, page 2 -> 25..49. Newest first - # via created_at. + statement = cls._apply_filters( select(cls).order_by(cls.created_at.desc(),cls.id.desc()), search, isread, application_status, assigned, is_duplicate, diff --git a/frontend/src/screens/Inbox.jsx b/frontend/src/screens/Inbox.jsx index 06bffb9..79c7883 100644 --- a/frontend/src/screens/Inbox.jsx +++ b/frontend/src/screens/Inbox.jsx @@ -1,8 +1,10 @@ /* ============================================================ Recruitment Inbox — application tabs over GET /inbox/all-applications. - Page size defaults to 10. Pagination (skip/offset) is independent of the - limit control except that page 2 uses the current limit: skip = (page-1)*limit. - Total comes from a count endpoint called once when the page opens. + Page size defaults to 10. skip/offset is the window start and is not + recomputed when Per page changes: growing 10 to 30 on a window that + started at row 11 requests skip=10, top=30 (rows 11–40). Clicking a + page number realigns skip = (page-1)*limit. Total comes from a count + endpoint called once when the page opens. ============================================================ */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -14,7 +16,7 @@ import EmailBody, { looksLikeHtml } from '../ui/EmailBody' import OpenResumeButton from '../ui/OpenResumeButton' import PageHeader from '../ui/PageHeader' import { Tabs } from '../ui/Tabs' -import { DEFAULT_PAGE_SIZE, Pagination, pageAfterSizeChange, pageWindow } from '../ui/DataTable' +import { DEFAULT_PAGE_SIZE, Pagination, pageWindow } from '../ui/DataTable' import { Avatar, Badge, EmptyState, Icon, ScoreChip, SkeletonRows } from '../ui/primitives' import { JobCard, PickRoleModal } from '../ui/SuggestedRoles' import { useToast } from '../ui/Toast' @@ -690,7 +692,7 @@ export default function Inbox() { const [channel, setChannel] = useState('email') const [formSheet, setFormSheet] = useState(DEFAULT_FORM_SHEET) const [tab, setTab] = useState('All Applications') - const [page, setPage] = useState(1) + const [skip, setSkip] = useState(0) const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE) const [selectedId, setSelectedId] = useState(null) const [q, setQ] = useState('') @@ -705,17 +707,17 @@ export default function Inbox() { const listParams = useMemo(() => ({ ...tabFilter, top: pageSize, - skip: (page - 1) * pageSize, + skip, ...(q.trim() ? { search: q.trim() } : {}), - }), [tabFilter, page, pageSize, q]) + }), [tabFilter, skip, pageSize, q]) const formParams = useMemo(() => ({ sheet: formSheet || undefined, - offset: (page - 1) * pageSize, + offset: skip, limit: pageSize, ...formTabFilter, ...(q.trim() ? { search: q.trim() } : {}), - }), [formSheet, page, pageSize, q, formTabFilter]) + }), [formSheet, skip, pageSize, q, formTabFilter]) const applicationsQuery = useQuery({ queryKey: qk.mailbox.applications(listParams), @@ -814,7 +816,14 @@ export default function Inbox() { ? (activeQuery.data?.total ?? 0) : (countsReady ? tabTotal : (poolTotal || (activeQuery.data?.total ?? 0))) const pages = Math.max(1, Math.ceil(total / pageSize)) - const currentPage = Math.min(page, pages) + const from = total ? skip + 1 : 0 + const to = Math.min(skip + pageSize, total) + const currentPage = Math.min(Math.floor(skip / pageSize) + 1, pages) + + useEffect(() => { + if (total <= 0 || skip < total) return + setSkip(Math.max(0, Math.floor((total - 1) / pageSize) * pageSize)) + }, [total, pageSize, skip]) const list = inbox @@ -848,7 +857,7 @@ export default function Inbox() { function switchChannel(next) { if (next === channel) return setChannel(next) - setPage(1) + setSkip(0) setSelectedId(null) setQ('') // Unread is email-only; leave it behind when opening Sheet Forms. @@ -987,7 +996,7 @@ export default function Inbox() { value={tab} onChange={(t) => { setTab(t) - setPage(1) + setSkip(0) setSelectedId(null) selection.clear() }} @@ -1019,7 +1028,7 @@ export default function Inbox() { value={formSheet} onChange={(e) => { setFormSheet(e.target.value) - setPage(1) + setSkip(0) setSelectedId(null) }} style={{ width: '100%' }} @@ -1034,7 +1043,7 @@ export default function Inbox() { { setQ(e.target.value); setPage(1) }} + onChange={(e) => { setQ(e.target.value); setSkip(0) }} placeholder={isForms ? 'Search name, email, position, city…' : 'Search applications…'} />
@@ -1102,18 +1111,17 @@ export default function Inbox() { {activeQuery.isSuccess && total > 0 && ( { setPage(p); setSelectedId(null); selection.clear() }} + setPage={(p) => { setSkip((p - 1) * pageSize); setSelectedId(null); selection.clear() }} pageButtons={pageWindow(currentPage, pages)} pageSize={pageSize} pageSizeMax={PAGE_SIZE_MAX} onPageSizeChange={(n) => { setPageSize(n) - setPage((p) => pageAfterSizeChange(p, total, n)) setSelectedId(null) selection.clear() }} -- 2.40.1