From 20923d04b67ac600c10e735c9dc51b6e6ee5a17b Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 31 Mar 2026 19:23:31 +0200 Subject: [PATCH] feat(dashboard): replace notifications fetch with stats in server load Removes /api/notifications from the dashboard widget fetches and replaces it with /api/stats so the page no longer needs to own notification data. Returns stats: StatsDTO | null (null on failure) instead of mentions. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/+page.server.ts | 16 +++++----- frontend/src/routes/page.server.spec.ts | 41 +++++++++++++++++++------ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index 1b106903..7824844e 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -3,7 +3,7 @@ import { createApiClient } from '$lib/api.server'; import type { components } from '$lib/generated/api'; type IncompleteDocumentDTO = components['schemas']['IncompleteDocumentDTO']; -type NotificationDTO = components['schemas']['NotificationDTO']; +type StatsDTO = components['schemas']['StatsDTO']; type Document = components['schemas']['Document']; export async function load({ url, fetch }) { @@ -55,19 +55,19 @@ export async function load({ url, fetch }) { const receiverObj = allPersons.find((p) => p.id === receiverId); // Dashboard widgets — failures are isolated and don't crash the page - let mentions: NotificationDTO[] = []; + let stats: StatsDTO | null = null; let incompleteDocs: IncompleteDocumentDTO[] = []; let recentDocs: Document[] = []; if (isDashboard) { - const [mentionsResult, incompleteResult, recentResult] = await Promise.allSettled([ - api.GET('/api/notifications', { params: { query: { size: 5 } } }), + const [statsResult, incompleteResult, recentResult] = await Promise.allSettled([ + api.GET('/api/stats'), api.GET('/api/documents/incomplete', { params: { query: { size: 5 } } }), api.GET('/api/documents/recent-activity', { params: { query: { size: 5 } } }) ]); - if (mentionsResult.status === 'fulfilled' && mentionsResult.value.response.ok) { - mentions = mentionsResult.value.data?.content ?? []; + if (statsResult.status === 'fulfilled' && statsResult.value.response.ok) { + stats = statsResult.value.data ?? null; } if (incompleteResult.status === 'fulfilled' && incompleteResult.value.response.ok) { incompleteDocs = incompleteResult.value.data ?? []; @@ -80,7 +80,7 @@ export async function load({ url, fetch }) { return { isDashboard, documents, - mentions, + stats, incompleteDocs, recentDocs, initialValues: { @@ -96,7 +96,7 @@ export async function load({ url, fetch }) { return { isDashboard, documents: [], - mentions: [], + stats: null, incompleteDocs: [], recentDocs: [], initialValues: { senderName: '', receiverName: '' }, diff --git a/frontend/src/routes/page.server.spec.ts b/frontend/src/routes/page.server.spec.ts index 63d7abd6..24b2e0ad 100644 --- a/frontend/src/routes/page.server.spec.ts +++ b/frontend/src/routes/page.server.spec.ts @@ -22,14 +22,14 @@ function makeUrl(params: Record = {}) { // ─── dashboard mode (no search filters) ────────────────────────────────────── describe('home page load — dashboard mode', () => { - it('sets isDashboard true and fetches all three widget APIs', async () => { + it('sets isDashboard true and fetches stats, incomplete, and recent APIs', async () => { const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons .mockResolvedValueOnce({ response: { ok: true }, - data: { content: [{ id: 'n1' }] } - }) // notifications + data: { totalDocuments: 42, totalPersons: 7 } + }) // stats .mockResolvedValueOnce({ response: { ok: true }, data: [{ id: 'd1' }] }) // incomplete .mockResolvedValueOnce({ response: { ok: true }, data: [{ id: 'd2' }] }); // recent vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< @@ -39,17 +39,20 @@ describe('home page load — dashboard mode', () => { const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch }); expect(result.isDashboard).toBe(true); - expect(result.mentions).toHaveLength(1); + expect(result.stats).toEqual({ totalDocuments: 42, totalPersons: 7 }); expect(result.incompleteDocs).toHaveLength(1); expect(result.recentDocs).toHaveLength(1); expect(result.documents).toEqual([]); }); - it('defaults mentions to [] when notifications API rejects', async () => { + it('returns stats with totalDocuments from /api/stats', async () => { const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons - .mockRejectedValueOnce(new Error('network')) // notifications + .mockResolvedValueOnce({ + response: { ok: true }, + data: { totalDocuments: 248, totalPersons: 34 } + }) // stats .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // incomplete .mockResolvedValueOnce({ response: { ok: true }, data: [] }); // recent vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< @@ -58,7 +61,24 @@ describe('home page load — dashboard mode', () => { const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch }); - expect(result.mentions).toEqual([]); + expect(result.stats?.totalDocuments).toBe(248); + expect(result.stats?.totalPersons).toBe(34); + }); + + it('returns stats: null when /api/stats rejects', async () => { + const mockGet = vi + .fn() + .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons + .mockRejectedValueOnce(new Error('network')) // stats + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // incomplete + .mockResolvedValueOnce({ response: { ok: true }, data: [] }); // recent + vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< + typeof createApiClient + >); + + const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch }); + + expect(result.stats).toBeNull(); }); it('defaults incompleteDocs to [] when incomplete API rejects', async () => { @@ -81,7 +101,10 @@ describe('home page load — dashboard mode', () => { const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons - .mockResolvedValueOnce({ response: { ok: true }, data: { content: [] } }) // notifications + .mockResolvedValueOnce({ + response: { ok: true }, + data: { totalDocuments: 0, totalPersons: 0 } + }) // stats .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // incomplete .mockRejectedValueOnce(new Error('network')); // recent vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< @@ -113,7 +136,7 @@ describe('home page load — search mode', () => { expect(result.isDashboard).toBe(false); expect(result.documents).toHaveLength(1); - expect(result.mentions).toEqual([]); + expect(result.stats).toBeNull(); expect(result.incompleteDocs).toEqual([]); expect(result.recentDocs).toEqual([]); // Only two API calls — no widget calls