diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx
index 707e19c..7f16291 100644
--- a/job-tracker-ui/src/App.tsx
+++ b/job-tracker-ui/src/App.tsx
@@ -60,7 +60,6 @@ const AdminAuditPage = lazy(() => import("./views/AdminAuditPage"));
const AdminUsersPage = lazy(() => import("./views/AdminUsersPage"));
const AdminSystemPage = lazy(() => import("./views/AdminSystemPage"));
const CorrespondenceInboxPage = lazy(() => import("./views/CorrespondenceInboxPage"));
-const GmailReviewPage = lazy(() => import("./views/GmailReviewPage"));
const OperationsPage = lazy(() => import("./views/OperationsPage"));
const NotFoundPage = lazy(() => import("./views/NotFoundPage"));
@@ -359,7 +358,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
} />
} />
} />
- } />
+ } />
} />
} />
} />
diff --git a/job-tracker-ui/src/correspondence-inbox-page.test.tsx b/job-tracker-ui/src/correspondence-inbox-page.test.tsx
index 944bcf1..5671554 100644
--- a/job-tracker-ui/src/correspondence-inbox-page.test.tsx
+++ b/job-tracker-ui/src/correspondence-inbox-page.test.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import '@testing-library/jest-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
-import { MemoryRouter } from 'react-router-dom';
+import { MemoryRouter, Navigate, Route, Routes } from 'react-router-dom';
import { ToastProvider } from './toast';
import { I18nProvider } from './i18n/I18nProvider';
import CorrespondenceInboxPage from './views/CorrespondenceInboxPage';
@@ -35,8 +35,8 @@ function renderPage() {
describe('CorrespondenceInboxPage', () => {
beforeEach(() => {
- mockedApi.get.mockResolvedValue({
- data: [
+ mockedApi.get.mockImplementation((url: string) => {
+ if (url === '/correspondence') return Promise.resolve({ data: [
{
id: 1,
jobApplicationId: 42,
@@ -54,8 +54,13 @@ describe('CorrespondenceInboxPage', () => {
labelCount: 2,
attachmentCount: 1,
},
- ],
- } as any);
+ ] } as any);
+ if (url === '/gmail/review-candidates') return Promise.resolve({ data: {
+ queries: [], candidateThreadCount: 0, autoLinkThreadCount: 0, reviewThreadCount: 0, unmatchedThreadCount: 0, threads: [],
+ } } as any);
+ if (url === '/gmail/suggested-jobs') return Promise.resolve({ data: { count: 0, items: [] } } as any);
+ return Promise.resolve({ data: {} } as any);
+ });
});
afterEach(() => {
@@ -65,7 +70,7 @@ describe('CorrespondenceInboxPage', () => {
test('renders correspondence inbox items and reloads with filters', async () => {
renderPage();
- expect(await screen.findByText(/correspondence inbox/i)).toBeInTheDocument();
+ expect(await screen.findByRole('heading', { name: /job email/i })).toBeInTheDocument();
expect(await screen.findByText(/1 items/i)).toBeInTheDocument();
expect(await screen.findByText(/acme systems/i)).toBeInTheDocument();
expect(await screen.findByText(/backend engineer/i)).toBeInTheDocument();
@@ -85,4 +90,31 @@ describe('CorrespondenceInboxPage', () => {
}));
});
});
+
+ test('uses one hub for linked messages and recruitment suggestions', async () => {
+ renderPage();
+
+ fireEvent.click(await screen.findByRole('button', { name: /review suggestions/i }));
+ expect(await screen.findByRole('heading', { name: /recruitment message review/i })).toBeInTheDocument();
+ expect(mockedApi.get).toHaveBeenCalledWith('/gmail/review-candidates');
+ expect(screen.getByRole('button', { name: /linked messages/i })).toBeInTheDocument();
+ });
+
+ test('legacy Gmail review route redirects into the consolidated hub filter', async () => {
+ render(
+
+
+
+
+ } />
+ } />
+
+
+
+ ,
+ );
+
+ expect(await screen.findByRole('heading', { name: /recruitment message review/i })).toBeInTheDocument();
+ expect(mockedApi.get).not.toHaveBeenCalledWith('/correspondence', expect.anything());
+ });
});
diff --git a/job-tracker-ui/src/views/CorrespondenceInboxPage.tsx b/job-tracker-ui/src/views/CorrespondenceInboxPage.tsx
index d426be6..4e4fe7d 100644
--- a/job-tracker-ui/src/views/CorrespondenceInboxPage.tsx
+++ b/job-tracker-ui/src/views/CorrespondenceInboxPage.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useSearchParams } from "react-router-dom";
import {
Box,
Chip,
@@ -17,6 +17,7 @@ import {
import MailOutlineIcon from "@mui/icons-material/MailOutline";
import { api, getApiErrorMessage } from "../api";
import { useToast } from "../toast";
+import GmailReviewPage from "./GmailReviewPage";
export type CorrespondenceInboxItem = {
id: number;
@@ -38,6 +39,8 @@ export type CorrespondenceInboxItem = {
export default function CorrespondenceInboxPage() {
const navigate = useNavigate();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const view = searchParams.get("view") === "review" ? "review" : "inbox";
const { toast } = useToast();
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
@@ -64,8 +67,8 @@ export default function CorrespondenceInboxPage() {
}, [direction, linkState, query, toast]);
useEffect(() => {
- void load();
- }, [load]);
+ if (view === "inbox") void load();
+ }, [load, view]);
const filteredSummary = useMemo(() => {
const linked = items.filter((item) => item.externalThreadId).length;
@@ -85,20 +88,22 @@ export default function CorrespondenceInboxPage() {
>
- Correspondence inbox
+ Job email
- Cross-job view of imported correspondence and Gmail-linked history.
+ Review linked correspondence and suggested recruitment messages in one place.
- } label={`${items.length} items`} variant="outlined" />
- 0 ? "success" : "default"} />
-
-
+ {view === "inbox" ? } label={`${items.length} items`} variant="outlined" /> : null}
+ {view === "inbox" ? 0 ? "success" : "default"} /> : null}
+ {view === "inbox" ? : null}
+
+
-
+ {view === "review" ? : <>
+
setQuery(e.target.value)} placeholder="Company, role, recruiter, subject" />
Direction
@@ -118,7 +123,7 @@ export default function CorrespondenceInboxPage() {
-
+
{loading ? : null}
@@ -148,6 +153,7 @@ export default function CorrespondenceInboxPage() {
))}
+ >}
);
}
diff --git a/job-tracker-ui/src/views/GmailReviewPage.tsx b/job-tracker-ui/src/views/GmailReviewPage.tsx
index ccecce2..fd0b7ea 100644
--- a/job-tracker-ui/src/views/GmailReviewPage.tsx
+++ b/job-tracker-ui/src/views/GmailReviewPage.tsx
@@ -5,7 +5,7 @@ import { CreatedSuggestedGmailJobResult, GmailManualSyncResult, GmailReviewQueue
import { useToast } from "../toast";
import { useNavigate } from "react-router-dom";
-export default function GmailReviewPage() {
+export default function GmailReviewPage({ embedded = false }: { embedded?: boolean }) {
const { toast } = useToast();
const navigate = useNavigate();
const [data, setData] = useState(null);
@@ -134,7 +134,7 @@ export default function GmailReviewPage() {
>
- Gmail review queue
+ {embedded ? "Recruitment message review" : "Gmail review queue"}
Manual sync, high-confidence auto-linking, medium-confidence review, and suggested jobs from unmatched Gmail threads.
@@ -146,7 +146,7 @@ export default function GmailReviewPage() {
-
+ {!embedded ? : null}