Merge pull request 'Inbox: All channel � Email and Sheet Forms merged in one queue' (#53) from Talha into main
Deploy to S3 / deploy (push) Successful in 33s Details

pull/64/head^2
talha.ahmed 2026-09-02 12:36:29 +00:00
commit 5f77ba9f8e
1 changed files with 87 additions and 42 deletions

View File

@ -59,10 +59,11 @@ function storeSyncRunId(id) {
}
}
/** Inbox channel: Outlook email queue vs imported Google Form rows. */
/** Inbox channel: Outlook email queue, imported Google Form rows, or both. */
const CHANNELS = [
{ key: 'email', label: 'Email', icon: 'mail' },
{ key: 'forms', label: 'Sheet Forms', icon: 'layers' },
{ key: 'all', label: 'All', icon: 'inbox' },
]
const DEFAULT_FORM_SHEET = 'Form Responses - Candidate Database Sheet 2026'
@ -303,6 +304,7 @@ async function fetchMessageDetail(recordId) {
if (!row) return null
const name = row.sender_name || row.fromEmail || 'Unknown'
return {
kind: 'email',
id: String(row.id),
name,
initials: initialsOf(name),
@ -356,6 +358,7 @@ async function fetchApplications(params) {
rows: rows.map((row) => {
const name = row.name || row.email || 'Unknown'
return {
kind: 'email',
id: String(row.id),
name,
initials: initialsOf(name),
@ -737,25 +740,31 @@ export default function Inbox() {
const [noting, setNoting] = useState(null)
const isForms = channel === 'forms'
const channelTabs = isForms ? FORM_TABS : TABS
// Combined channel: both sources fetched UNPAGED (each endpoint reads a
// missing top/limit as no LIMIT), merged by date, and paged client-side
// per-source skip/top cannot compose into a correct global page.
const isAllChannel = channel === 'all'
const channelTabs = isForms || isAllChannel ? FORM_TABS : TABS
const tabFilter = TAB_FILTERS[tab] ?? {}
const formTabFilter = FORM_TAB_FILTERS[tab] ?? {}
const listParams = useMemo(() => ({
...tabFilter,
// 'all' drops the param entirely the endpoint reads a missing top as unpaged.
top: pageSize === 'all' ? undefined : pageSize,
skip,
// 'all' page size and the All channel drop the param entirely the
// endpoint reads a missing top as unpaged.
top: pageSize === 'all' || isAllChannel ? undefined : pageSize,
skip: isAllChannel ? 0 : skip,
...(q.trim() ? { search: q.trim() } : {}),
}), [tabFilter, skip, pageSize, q])
}), [tabFilter, skip, pageSize, q, isAllChannel])
const formParams = useMemo(() => ({
sheet: formSheet || undefined,
offset: skip,
limit: pageSize === 'all' ? undefined : pageSize,
// All channel spans every sheet tab, not just the selected one.
sheet: isAllChannel ? undefined : (formSheet || undefined),
offset: isAllChannel ? 0 : skip,
limit: pageSize === 'all' || isAllChannel ? undefined : pageSize,
...formTabFilter,
...(q.trim() ? { search: q.trim() } : {}),
}), [formSheet, skip, pageSize, q, formTabFilter])
}), [formSheet, skip, pageSize, q, formTabFilter, isAllChannel])
const applicationsQuery = useQuery({
queryKey: qk.mailbox.applications(listParams),
@ -776,7 +785,7 @@ export default function Inbox() {
const formQuery = useQuery({
queryKey: qk.mailbox.formData(formParams),
queryFn: () => fetchFormApplications(formParams),
enabled: isForms,
enabled: isForms || isAllChannel,
})
const countsQuery = useQuery({
@ -785,13 +794,14 @@ export default function Inbox() {
enabled: !isForms,
})
const formCountsSheet = isAllChannel ? undefined : (formSheet || undefined)
const formCountsQuery = useQuery({
queryKey: qk.mailbox.formCounts({ sheet: formSheet || undefined }),
queryKey: qk.mailbox.formCounts({ sheet: formCountsSheet }),
queryFn: async () => {
const res = await sheetApi.fetchFormCounts({ sheet: formSheet || undefined })
const res = await sheetApi.fetchFormCounts({ sheet: formCountsSheet })
return res?.data ?? {}
},
enabled: isForms,
enabled: isForms || isAllChannel,
})
const emailTotalQuery = useQuery({
@ -805,12 +815,12 @@ export default function Inbox() {
})
const formTotalQuery = useQuery({
queryKey: qk.mailbox.formTotal({ sheet: formSheet || undefined }),
queryKey: qk.mailbox.formTotal({ sheet: formCountsSheet }),
queryFn: async () => {
const res = await sheetApi.countFormData({ sheet: formSheet || undefined })
const res = await sheetApi.countFormData({ sheet: formCountsSheet })
return typeof res?.total === 'number' ? res.total : (res?.data?.total ?? 0)
},
enabled: isForms,
enabled: isForms || isAllChannel,
staleTime: Infinity,
})
@ -829,27 +839,55 @@ export default function Inbox() {
}
}, [isForms, formSheetsQuery.data, formSheet])
const activeQuery = isForms ? formQuery : applicationsQuery
// All channel: both sources arrive unpaged; merge newest-first and let the
// pager slice the merged array below.
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 : []
return [...emails, ...forms].sort(
(a, b) => (b.received?.getTime() ?? 0) - (a.received?.getTime() ?? 0),
)
}, [isAllChannel, applicationsQuery.data, formQuery.data])
const activeQuery = isAllChannel
? {
isPending: applicationsQuery.isPending || formQuery.isPending,
// one healthy source still renders; error only when both are down
isError: applicationsQuery.isError && formQuery.isError,
isSuccess: applicationsQuery.isSuccess && formQuery.isSuccess,
error: applicationsQuery.error ?? formQuery.error,
data: { rows: mergedRows ?? [], total: mergedRows?.length ?? 0 },
}
: (isForms ? formQuery : applicationsQuery)
const inbox = Array.isArray(activeQuery.data?.rows) ? activeQuery.data.rows : []
const serverCounts = isForms ? (formCountsQuery.data ?? {}) : (countsQuery.data ?? {})
const counts = useMemo(
() => {
const n = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
const e = countsQuery.data ?? {}
const f = formCountsQuery.data ?? {}
const pick = (key) => (isAllChannel
? n(e[key]) + n(f[key])
: n((isForms ? f : e)[key]))
return {
'All Applications': n(serverCounts.all),
Unread: n(serverCounts.unread),
Processed: n(serverCounts.processed),
Rejected: n(serverCounts.rejected),
Duplicates: n(serverCounts.duplicates),
'All Applications': pick('all'),
Unread: pick('unread'),
Processed: pick('processed'),
Rejected: pick('rejected'),
Duplicates: pick('duplicates'),
}
},
[serverCounts],
[countsQuery.data, formCountsQuery.data, isForms, isAllChannel],
)
const poolTotal = isForms ? (formTotalQuery.data ?? 0) : (emailTotalQuery.data ?? 0)
const poolTotal = isAllChannel
? (emailTotalQuery.data ?? 0) + (formTotalQuery.data ?? 0)
: (isForms ? (formTotalQuery.data ?? 0) : (emailTotalQuery.data ?? 0))
const tabTotal = counts[tab] ?? 0
const countsReady = isForms ? formCountsQuery.isSuccess : countsQuery.isSuccess
const countsReady = isAllChannel
? countsQuery.isSuccess && formCountsQuery.isSuccess
: (isForms ? formCountsQuery.isSuccess : countsQuery.isSuccess)
const total = q.trim()
? (activeQuery.data?.total ?? 0)
: (countsReady ? tabTotal : (poolTotal || (activeQuery.data?.total ?? 0)))
@ -867,11 +905,15 @@ export default function Inbox() {
setSkip(Math.max(0, Math.floor((total - 1) / pageSize) * pageSize))
}, [total, pageSize, skip, showAll])
const list = inbox
// All channel pages the merged array client-side; single channels page on the server.
const list = isAllChannel && !showAll ? inbox.slice(skip, skip + pageSize) : inbox
// Mixed rows: the row's own kind picks the detail endpoint, not the channel.
const selectedKind = inbox.find((i) => i.id === selectedId)?.kind
?? (isForms ? 'form' : 'email')
const detailQuery = useQuery({
queryKey: isForms ? qk.mailbox.formRow(selectedId) : qk.mailbox.message(selectedId),
queryFn: () => (isForms ? fetchFormDetail(selectedId) : fetchMessageDetail(selectedId)),
queryKey: selectedKind === 'form' ? qk.mailbox.formRow(selectedId) : qk.mailbox.message(selectedId),
queryFn: () => (selectedKind === 'form' ? fetchFormDetail(selectedId) : fetchMessageDetail(selectedId)),
enabled: Boolean(selectedId),
})
@ -902,8 +944,8 @@ export default function Inbox() {
setSkip(0)
setSelectedId(null)
setQ('')
// Unread is email-only; leave it behind when opening Sheet Forms.
if (next === 'forms' && tab === 'Unread') setTab('All Applications')
// Unread is email-only; leave it behind when opening Sheet Forms or All.
if (next !== 'email' && tab === 'Unread') setTab('All Applications')
selection.clear()
}
@ -924,11 +966,12 @@ export default function Inbox() {
setReadAll.mutate({
read,
filter: { ...tabFilter, ...(q.trim() ? { search: q.trim() } : {}) },
ids: list.map((i) => i.id),
// sheet rows carry no mailbox read state email rows only
ids: list.filter((i) => i.kind !== 'form').map((i) => i.id),
})
return
}
const ids = list.map((i) => i.id)
const ids = list.filter((i) => i.kind !== 'form').map((i) => i.id)
if (!ids.length) return
setRead.mutate({ ids, read }, {
onSuccess: () => {
@ -975,8 +1018,8 @@ export default function Inbox() {
function select(id) {
setSelectedId(id)
if (isForms) return
const item = inbox.find((i) => i.id === id)
if (item?.kind === 'form') return // sheet rows have no mailbox read state
if (item?.unread) setRead.mutate({ ids: [id], read: true })
}
@ -1058,9 +1101,11 @@ export default function Inbox() {
<PageHeader
title="Recruitment Inbox"
sub={
isForms
? 'Google Form applicants — same queue energy, profile-first cards'
: 'Every candidate, every source — one unified queue'
isAllChannel
? 'Email and Sheet Forms together — one combined stream'
: isForms
? 'Google Form applicants — same queue energy, profile-first cards'
: 'Every candidate, every source — one unified queue'
}
actions={<>
<div className="pill-tabs" role="tablist" aria-label="Inbox channel">
@ -1184,7 +1229,7 @@ export default function Inbox() {
className={`inbox-item${i.unread ? ' unread' : ''}${selectedId === i.id ? ' active' : ''}`}
onClick={() => select(i.id)}
>
{!isForms && (
{i.kind !== 'form' && (
<RowCheck
checked={selection.selectedIds.has(i.id)}
onToggle={() => selection.toggle(i.id)}
@ -1207,7 +1252,7 @@ export default function Inbox() {
{i.atsScore != null && (
<div style={{ marginTop: 6 }}><ScoreChip score={i.atsScore} /></div>
)}
{isForms && i.noticePeriod && (
{i.kind === 'form' && i.noticePeriod && (
<div className="cell-sub" style={{ marginTop: 6 }}>{i.noticePeriod}</div>
)}
</div>
@ -1216,7 +1261,7 @@ export default function Inbox() {
{i.applicationStatus && i.applicationStatus !== 'CLOSED' && (
<Badge>{i.applicationStatus}</Badge>
)}
{isForms && i.residingCity && (
{i.kind === 'form' && i.residingCity && (
<span className="cell-sub">{i.residingCity}</span>
)}
</div>
@ -1262,7 +1307,7 @@ export default function Inbox() {
{friendlyAuthError(detailQuery.error, 'Request failed')}
</EmptyState>
</div>
) : isForms ? (
) : selected?.kind === 'form' ? (
<FormApplicantDetail
item={selected}
loading={detailQuery.isPending}