diff --git a/frontend/src/lib/generated/api.ts b/frontend/src/lib/generated/api.ts index 9c901d56..36c2996c 100644 --- a/frontend/src/lib/generated/api.ts +++ b/frontend/src/lib/generated/api.ts @@ -2471,6 +2471,8 @@ export interface components { pageSize: number; /** Format: int32 */ totalPages: number; + /** Format: int64 */ + undatedCount: number; }; MatchOffset: { /** Format: int32 */ diff --git a/frontend/src/routes/SearchFilterBar.svelte b/frontend/src/routes/SearchFilterBar.svelte index 344d6f26..5bec7245 100644 --- a/frontend/src/routes/SearchFilterBar.svelte +++ b/frontend/src/routes/SearchFilterBar.svelte @@ -16,6 +16,7 @@ let { tagQ = $bindable(''), tagOperator = $bindable<'AND' | 'OR'>('AND'), undated = $bindable(false), + undatedCount = 0, sort = $bindable('DATE'), dir = $bindable('desc'), showAdvanced = $bindable(false), @@ -37,6 +38,7 @@ let { tagQ?: string; tagOperator?: 'AND' | 'OR'; undated?: boolean; + undatedCount?: number; sort?: string; dir?: string; showAdvanced?: boolean; @@ -275,6 +277,18 @@ $effect(() => { {#if undated}✓{/if} {m.docs_filter_undated_only()} + + {#if undatedCount > 0} + {undatedCount} + {/if} diff --git a/frontend/src/routes/SearchFilterBar.svelte.spec.ts b/frontend/src/routes/SearchFilterBar.svelte.spec.ts index 11f4c32d..446cd046 100644 --- a/frontend/src/routes/SearchFilterBar.svelte.spec.ts +++ b/frontend/src/routes/SearchFilterBar.svelte.spec.ts @@ -162,6 +162,20 @@ describe('SearchFilterBar – undated-only toggle (#668)', () => { await page.getByTestId('undated-only-toggle').click(); await expect.poll(() => onSearchImmediate.mock.calls.length).toBeGreaterThan(0); }); + + it('shows the global undated count chip when undatedCount > 0', async () => { + // The count is the backend's global filtered total (#668), passed straight + // through — the chip must render it verbatim, not a page-derived number. + render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', undatedCount: 42 }); + await openAdvanced(); + await expect.element(page.getByTestId('undated-count')).toHaveTextContent('42'); + }); + + it('hides the undated count chip when undatedCount is 0', async () => { + render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', undatedCount: 0 }); + await openAdvanced(); + await expect.element(page.getByTestId('undated-count')).not.toBeInTheDocument(); + }); }); describe('SearchFilterBar – tagQ live filter', () => { diff --git a/frontend/src/routes/documents/+page.server.ts b/frontend/src/routes/documents/+page.server.ts index acd8f666..a4e2242b 100644 --- a/frontend/src/routes/documents/+page.server.ts +++ b/frontend/src/routes/documents/+page.server.ts @@ -85,6 +85,7 @@ export async function load({ url, fetch }) { pageNumber: 0, pageSize: PAGE_SIZE, totalPages: 0, + undatedCount: 0, q, from, to, @@ -116,6 +117,8 @@ export async function load({ url, fetch }) { pageNumber: result.data?.pageNumber ?? page, pageSize: result.data?.pageSize ?? PAGE_SIZE, totalPages: result.data?.totalPages ?? 0, + // Global undated count for the active filter, across all pages (issue #668). + undatedCount: result.data?.undatedCount ?? 0, q, from, to, diff --git a/frontend/src/routes/documents/+page.svelte b/frontend/src/routes/documents/+page.svelte index eabfea8c..5006d9eb 100644 --- a/frontend/src/routes/documents/+page.svelte +++ b/frontend/src/routes/documents/+page.svelte @@ -268,6 +268,7 @@ $effect(() => { bind:tagQ={tagQ} bind:tagOperator={tagOperator} bind:undated={undated} + undatedCount={data.undatedCount ?? 0} initialSenderName={initialSenderName} initialReceiverName={initialReceiverName} navKey={navKey} diff --git a/frontend/src/routes/documents/page.server.spec.ts b/frontend/src/routes/documents/page.server.spec.ts index 69726562..c51eb3f0 100644 --- a/frontend/src/routes/documents/page.server.spec.ts +++ b/frontend/src/routes/documents/page.server.spec.ts @@ -225,6 +225,51 @@ describe('documents page load — search params', () => { expect(result.totalElements).toBe(42); }); + it('forwards the global undatedCount from the search result (#668)', async () => { + // The backend returns the global undated total for the active filter across + // ALL pages; the loader must pass it straight through, not recompute it locally. + const mockGet = vi.fn().mockResolvedValue({ + response: { ok: true, status: 200 }, + data: { + items: [], + totalElements: 200, + pageNumber: 0, + pageSize: 50, + totalPages: 4, + undatedCount: 73 + } + }); + vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< + typeof createApiClient + >); + + const result = await load({ + url: makeUrl({ q: 'test' }), + request: new Request('http://localhost/documents'), + fetch: vi.fn() as unknown as typeof fetch + }); + + expect(result.undatedCount).toBe(73); + }); + + it('defaults undatedCount to 0 when the search result omits it', async () => { + const mockGet = vi.fn().mockResolvedValue({ + response: { ok: true, status: 200 }, + data: { items: [], totalElements: 0, pageNumber: 0, pageSize: 50, totalPages: 0 } + }); + vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< + typeof createApiClient + >); + + const result = await load({ + url: makeUrl(), + request: new Request('http://localhost/documents'), + fetch: vi.fn() as unknown as typeof fetch + }); + + expect(result.undatedCount).toBe(0); + }); + it('returns filter values in the result for pre-filling the UI', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 },