listing corrections

pull/87/head
ahmed.mujtaba 2026-09-09 20:16:25 +05:00
parent c0e4a94d58
commit 5fdc6031aa
11 changed files with 74 additions and 51 deletions

View File

@ -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 } })
}

View File

@ -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' }) {
)}
<div className="cell-sub">
{SOURCE_LABEL[item.source] || item.source || 'Application'}
{item.applied_at ? ` · ${fmtDateTime(toInstant(item.applied_at) || item.applied_at)}` : ''}
{item.applied_at ? ` · ${fmtDateTime(displayAppliedDate(item))}` : ''}
</div>
</div>
<Badge className={STAGE_BADGE[stage] || 'b-gray'}>{stage}</Badge>

View File

@ -240,7 +240,7 @@ export default function Assessments() {
</select>
</div>
</div>
<DataTable columns={columns} rows={rows} pageSize={8} empty="No assessments match these filters." />
<DataTable columns={columns} rows={rows} pageSize={50} empty="No assessments match these filters." />
</>
)}
</div>

View File

@ -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 1160). 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 51150). 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

View File

@ -322,7 +322,7 @@ export default function Interviews() {
<DataTable
columns={columns}
rows={rows}
pageSize={8}
pageSize={50}
empty="No interviews match these filters."
/>
)}

View File

@ -371,7 +371,7 @@ export default function JobBoard() {
</div>
)}
{!postsQuery.isPending && !postsQuery.isError && (
<DataTable columns={columns} rows={rows} pageSize={10} empty="No posts match these filters." />
<DataTable columns={columns} rows={rows} pageSize={50} empty="No posts match these filters." />
)}
</div>
</div>

View File

@ -379,7 +379,7 @@ export default function Jobs() {
<DataTable
columns={columns}
rows={rows}
pageSize={8}
pageSize={50}
empty="No requisitions match these filters."
onRowClick={(j) => setViewing(j)}
/>

View File

@ -291,7 +291,7 @@ export default function Offers() {
</div>
)}
{!offersQuery.isPending && !offersQuery.isError && (
<DataTable columns={columns} rows={rows} pageSize={8} empty="No offers match these filters." />
<DataTable columns={columns} rows={rows} pageSize={50} empty="No offers match these filters." />
)}
</div>

View File

@ -515,7 +515,7 @@ export default function Reports() {
</EmptyState>
</div>
) : (
<DataTable columns={reportColumns} rows={reportsQuery.data} pageSize={8} />
<DataTable columns={reportColumns} rows={reportsQuery.data} pageSize={50} />
)}
</div>
@ -595,7 +595,7 @@ export default function Reports() {
</EmptyState>
</div>
) : (
<DataTable columns={deptColumns} rows={deptRows} pageSize={10} />
<DataTable columns={deptColumns} rows={deptRows} pageSize={50} />
)}
</div>
@ -631,7 +631,7 @@ export default function Reports() {
</EmptyState>
</div>
) : (
<DataTable columns={costColumns} rows={costTotals.map((r) => ({ id: r.type, ...r }))} pageSize={10} />
<DataTable columns={costColumns} rows={costTotals.map((r) => ({ id: r.type, ...r }))} pageSize={50} />
)}
</div>
@ -664,7 +664,7 @@ export default function Reports() {
<DataTable
columns={sourceColumns}
rows={(sourcesQuery.data ?? []).map((r) => ({ ...r, id: r.id ?? r.source }))}
pageSize={10}
pageSize={50}
/>
)}
</div>
@ -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}
/>
) : (
<EmptyState icon="inbox" title="No rows in this window">

View File

@ -246,7 +246,7 @@ export default function Requisitions() {
</select>
</div>
</div>
<DataTable columns={columns} rows={rows} pageSize={8} empty="No requisitions match these filters." />
<DataTable columns={columns} rows={rows} pageSize={50} empty="No requisitions match these filters." />
</>
)}
</div>

View File

@ -7,7 +7,7 @@
useDataTable alone. The other six consumers use <DataTable/>.
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]