diff --git a/frontend/src/api/candidates.js b/frontend/src/api/candidates.js index 92d83ec..989dc76 100644 --- a/frontend/src/api/candidates.js +++ b/frontend/src/api/candidates.js @@ -125,7 +125,7 @@ export function viewCvBankCv(id) { * Needs candidates.view. `assigned` is tri-valued: omit for all, false for * still in the bank, true for rows that already have a job_post_id. */ -export function listMatching({ search, top = 10, skip = 0, assigned } = {}) { +export function listMatching({ search, top = 50, skip = 0, assigned } = {}) { return request('/candidate/matching/fetch', { params: { search, top, skip, assigned }, }) @@ -192,7 +192,7 @@ export function toCandidateView(row) { } -export function listCandidateUsers({ roleId = 8, top = 10, skip = 0, assignedJobPostId } = {}) { +export function listCandidateUsers({ roleId = 8, top = 50, skip = 0, assignedJobPostId } = {}) { return request('/candidate/fetch/users', { params: { role_id: roleId, top, skip, assigned_job_post_id: assignedJobPostId }, }) @@ -536,7 +536,7 @@ export function createActivity({ inboxId, type, status, description }) { * detail query refetches on every write in the modal. Fetched lazily when the * History tab opens, paginated server-side. */ -export function listHistory(userId, { limit = 10, offset = 0 } = {}) { +export function listHistory(userId, { limit = 50, offset = 0 } = {}) { return request('/candidate/history/fetch', { params: { user_id: userId, limit, offset } }) } diff --git a/frontend/src/components/ReapplicantHistory.jsx b/frontend/src/components/ReapplicantHistory.jsx index 8eaeffd..7fd159a 100644 --- a/frontend/src/components/ReapplicantHistory.jsx +++ b/frontend/src/components/ReapplicantHistory.jsx @@ -64,7 +64,7 @@ export function candidateApplicationsOf(row) { const self = syntheticCurrentApplication(row) if (self) items.push(self) } - items.sort((a, b) => (appliedAtMs(b?.applied_at) ?? 0) - (appliedAtMs(a?.applied_at) ?? 0)) + items.sort((a, b) => (appliedAtMs(b) ?? 0) - (appliedAtMs(a) ?? 0)) return items } @@ -101,15 +101,26 @@ function syntheticCurrentApplication(row) { } } -function appliedAtMs(value) { +function isUtcSource(source) { + return source === 'inbox' || source === 'filtered' +} + +/** Email Graph stamps are UTC; sheet/manual stamps are wall-clock digits. */ +function displayAppliedDate(item) { + const value = item?.applied_at if (value == null || value === '') return null if (value instanceof Date) { - return Number.isNaN(value.getTime()) ? null : value.getTime() + return Number.isNaN(value.getTime()) ? null : value } - const instant = toInstant(value) - if (instant) return instant.getTime() - const wall = toDate(value) - return wall ? wall.getTime() : null + if (isUtcSource(item?.source)) { + return toInstant(value) || toDate(value) + } + return toDate(value) || toInstant(value) +} + +function appliedAtMs(item) { + const d = displayAppliedDate(item) + return d ? d.getTime() : null } function currentRowIds(row) { @@ -273,7 +284,7 @@ export function PreviousApplications({ row, title = 'Total applications' }) { )}
{SOURCE_LABEL[item.source] || item.source || 'Application'} - {item.applied_at ? ` · ${fmtDateTime(toInstant(item.applied_at) || item.applied_at)}` : ''} + {item.applied_at ? ` · ${fmtDateTime(displayAppliedDate(item))}` : ''}
{stage} diff --git a/frontend/src/screens/Assessments.jsx b/frontend/src/screens/Assessments.jsx index 067f1b5..e314cbc 100644 --- a/frontend/src/screens/Assessments.jsx +++ b/frontend/src/screens/Assessments.jsx @@ -240,7 +240,7 @@ export default function Assessments() { - + )} diff --git a/frontend/src/screens/Inbox.jsx b/frontend/src/screens/Inbox.jsx index f773393..13d7dcd 100644 --- a/frontend/src/screens/Inbox.jsx +++ b/frontend/src/screens/Inbox.jsx @@ -1,9 +1,9 @@ /* ============================================================ Recruitment Inbox — application tabs over GET /inbox/all-applications. - Page size defaults to 10 (dropdown: 10 / 50 / 100). skip/offset is the - window start and is not recomputed when Per page changes: growing 10 to - 50 on a window that started at row 11 requests skip=10, top=50 - (rows 11–60). Clicking a + Page size defaults to 50 (dropdown: 10 / 50 / 100). skip/offset is the + window start and is not recomputed when Per page changes: growing 50 to + 100 on a window that started at row 51 requests skip=50, top=100 + (rows 51–150). Clicking a page number realigns skip = (page-1)*limit. Total comes from a count endpoint called once when the page opens. ============================================================ */ @@ -51,8 +51,10 @@ const PAGE_SIZE_MAX = 500 * qk.mailbox.all() the moment a run completes — so refetching on every visit * bought nothing and cost a full-width skeleton each time. * - * List fetches always send the UI page size (10 / 50 / 100), including All — - * omitting limit used to dump the whole form_data table into the browser. + * List fetches send the UI page size (10 / 50 / 100). All channel is + * different: email and sheet are fetched as two pools (up to PAGE_SIZE_MAX), + * sorted by received time descending, then sliced to the page so a page is + * not "half inbox, half form". * * staleTime therefore covers a normal working stretch, and keepPreviousData * means a tab switch, a page turn or a keystroke re-renders the rows already @@ -301,6 +303,20 @@ function formReceivedAt(entryDate, entryTime, timestampRaw) { return d } +/** Newest-first clock for All-channel merge. Prefer applied/received, not import time. */ +function rowTimeMs(row) { + const values = [row?.received, row?.applied, row?.applied_at, row?.createdAt] + for (const v of values) { + if (v instanceof Date && !Number.isNaN(v.getTime())) return v.getTime() + if (typeof v === 'number' && Number.isFinite(v)) return v + if (typeof v === 'string' && v.trim()) { + const d = row?.kind === 'form' ? (toDate(v) || toInstant(v)) : (toInstant(v) || toDate(v)) + if (d) return d.getTime() + } + } + return 0 +} + /** Same numeric gate as email `ats_score` / pipeline ScoreChip. */ function asAtsScore(value) { if (value == null || value === '') return null @@ -1227,12 +1243,12 @@ export default function Inbox() { }, [deepOpen, deepKind, setSearchParams]) const isForms = channel === 'forms' - // Combined channel: email and form lists both arrive newest created_at - // first; the merge uses that same clock so a June form cannot sit above a - // later email just because it was on the first sheet page. const isAllChannel = channel === 'all' const channelTabs = isForms || isAllChannel ? FORM_TABS : TABS const pageLimit = pageSize === 'all' ? undefined : pageSize + // All: pull a merge pool from offset 0, sort desc by received, then slice. + const fetchTop = isAllChannel && pageLimit != null ? PAGE_SIZE_MAX : pageLimit + const fetchSkip = isAllChannel ? 0 : (pageLimit == null ? 0 : skip) const activeInboxFilters = [ inboxFilters.location, inboxFilters.source, @@ -1244,14 +1260,13 @@ export default function Inbox() { const formTabFilter = FORM_TAB_FILTERS[tab] ?? {} const listParams = useMemo(() => ({ ...tabFilter, - // Show-all omits top (no LIMIT). Otherwise send the pager size — 10, 50, 100. - top: pageLimit, - skip: pageLimit == null ? 0 : skip, + top: fetchTop, + skip: fetchSkip, ...(search ? { search } : {}), ...(city ? { city } : {}), ...(source ? { source } : {}), ...assignedParams, - }), [tabFilter, skip, pageLimit, search, city, source, assignedParams]) + }), [tabFilter, fetchSkip, fetchTop, search, city, source, assignedParams]) /** * Sheet Forms only. On the All channel these rows are merged with email ones, @@ -1268,15 +1283,15 @@ export default function Inbox() { const formParams = useMemo(() => ({ // All channel spans every sheet tab, not just the selected one. sheet: isAllChannel ? undefined : (formSheet || undefined), - offset: pageLimit == null ? 0 : skip, - limit: pageLimit, + offset: fetchSkip, + limit: fetchTop, ...formTabFilter, ...(search ? { search } : {}), ...(city ? { city } : {}), ...(source ? { source } : {}), ...assignedParams, ...linkFilters, - }), [formSheet, skip, pageLimit, search, city, source, assignedParams, formTabFilter, isAllChannel, linkFilters]) + }), [formSheet, fetchSkip, fetchTop, search, city, source, assignedParams, formTabFilter, isAllChannel, linkFilters]) const citiesQuery = useQuery({ queryKey: qk.mailbox.cities(), @@ -1385,18 +1400,15 @@ export default function Inbox() { } }, [isForms, formSheetsQuery.data, formSheet]) - // All channel: one page from each source, newest created_at first, then merged. + // All channel: merge email + sheet pools, newest received first, then page. const mergedRows = useMemo(() => { if (!isAllChannel) return null const emails = Array.isArray(applicationsQuery.data?.rows) ? applicationsQuery.data.rows : [] const forms = Array.isArray(formQuery.data?.rows) ? formQuery.data.rows : [] - const when = (row) => { - const d = row.createdAt || row.received - const t = d instanceof Date ? d.getTime() : NaN - return Number.isFinite(t) ? t : 0 - } - return [...emails, ...forms].sort((a, b) => when(b) - when(a)) - }, [isAllChannel, applicationsQuery.data, formQuery.data]) + const sorted = [...emails, ...forms].sort((a, b) => rowTimeMs(b) - rowTimeMs(a)) + if (pageSize === 'all') return sorted + return sorted.slice(skip, skip + pageSize) + }, [isAllChannel, applicationsQuery.data, formQuery.data, skip, pageSize]) const activeQuery = isAllChannel ? { @@ -1470,9 +1482,9 @@ export default function Inbox() { setSkip(Math.max(0, Math.floor((total - 1) / pageSize) * pageSize)) }, [total, pageSize, skip, showAll]) - // Server already applied skip/limit (or the whole tab when Show all). + // Email / Forms: server already applied skip/limit. All: client slice after merge. const list = inbox - const to = showAll ? total : Math.min(skip + (isAllChannel ? list.length : pageSize), total) + const to = showAll ? total : Math.min(skip + list.length, total) // Mixed rows: the row's own kind picks the detail endpoint, not the channel. const selectedKind = inbox.find((i) => sameInboxId(i.id, selectedId))?.kind diff --git a/frontend/src/screens/Interviews.jsx b/frontend/src/screens/Interviews.jsx index 00a8084..5137dd5 100644 --- a/frontend/src/screens/Interviews.jsx +++ b/frontend/src/screens/Interviews.jsx @@ -322,7 +322,7 @@ export default function Interviews() { )} diff --git a/frontend/src/screens/JobBoard.jsx b/frontend/src/screens/JobBoard.jsx index 51a835e..d51518e 100644 --- a/frontend/src/screens/JobBoard.jsx +++ b/frontend/src/screens/JobBoard.jsx @@ -371,7 +371,7 @@ export default function JobBoard() { )} {!postsQuery.isPending && !postsQuery.isError && ( - + )} diff --git a/frontend/src/screens/Jobs.jsx b/frontend/src/screens/Jobs.jsx index 8bc066e..27f5ea2 100644 --- a/frontend/src/screens/Jobs.jsx +++ b/frontend/src/screens/Jobs.jsx @@ -379,7 +379,7 @@ export default function Jobs() { setViewing(j)} /> diff --git a/frontend/src/screens/Offers.jsx b/frontend/src/screens/Offers.jsx index cda2328..b05af5b 100644 --- a/frontend/src/screens/Offers.jsx +++ b/frontend/src/screens/Offers.jsx @@ -291,7 +291,7 @@ export default function Offers() { )} {!offersQuery.isPending && !offersQuery.isError && ( - + )} diff --git a/frontend/src/screens/Reports.jsx b/frontend/src/screens/Reports.jsx index 82938f6..0f51bf5 100644 --- a/frontend/src/screens/Reports.jsx +++ b/frontend/src/screens/Reports.jsx @@ -515,7 +515,7 @@ export default function Reports() { ) : ( - + )} @@ -595,7 +595,7 @@ export default function Reports() { ) : ( - + )} @@ -631,7 +631,7 @@ export default function Reports() { ) : ( - ({ id: r.type, ...r }))} pageSize={10} /> + ({ id: r.type, ...r }))} pageSize={50} /> )} @@ -664,7 +664,7 @@ export default function Reports() { ({ ...r, id: r.id ?? r.source }))} - pageSize={10} + pageSize={50} /> )} @@ -722,7 +722,7 @@ export default function Reports() { : String(row[c.key])), }))} rows={runResult.rows.map((row, i) => ({ id: i, ...row }))} - pageSize={10} + pageSize={50} /> ) : ( diff --git a/frontend/src/screens/Requisitions.jsx b/frontend/src/screens/Requisitions.jsx index b2ed6ec..4300761 100644 --- a/frontend/src/screens/Requisitions.jsx +++ b/frontend/src/screens/Requisitions.jsx @@ -246,7 +246,7 @@ export default function Requisitions() { - + )} diff --git a/frontend/src/ui/DataTable.jsx b/frontend/src/ui/DataTable.jsx index 7cbf90a..a64778d 100644 --- a/frontend/src/ui/DataTable.jsx +++ b/frontend/src/ui/DataTable.jsx @@ -7,7 +7,7 @@ useDataTable alone. The other six consumers use . Sort comparator and the ellipsis pager windowing are ported verbatim. - Page size defaults to 10 (the GET `top`/`limit` default) and is user-settable; + Page size defaults to 50 and is user-settable (10 / 50 / 100); screens that paginate on the server pass the same value as the query param. ============================================================ */ @@ -15,8 +15,8 @@ import { useEffect, useMemo, useState } from 'react' import Icon from './icons' import { EmptyState } from './primitives' -/** Matches the backend Query(10) default on list GET endpoints. */ -export const DEFAULT_PAGE_SIZE = 10 +/** Default Per page value on every listing. 10 remains in PAGE_SIZE_OPTIONS. */ +export const DEFAULT_PAGE_SIZE = 50 /** Fixed Per page choices — a dropdown, not a free-text box. */ export const PAGE_SIZE_OPTIONS = [10, 50, 100]