diff --git a/client/src/pages/dashboard/useDashboard.test.tsx b/client/src/pages/dashboard/useDashboard.test.tsx index 01f9591b2..17cb741fc 100644 --- a/client/src/pages/dashboard/useDashboard.test.tsx +++ b/client/src/pages/dashboard/useDashboard.test.tsx @@ -370,4 +370,69 @@ describe('useDashboard', () => { act(() => result.current.setTripFilter('archive')); expect(result.current.gridTrips[0].cover_image).toBe('/uploads/covers/new-rome.jpg'); }); + it('FE-HOOK-DASH-027: creating, copying or redating a trip refreshes the stats and upcoming bookings (#2604)', async () => { + let statsCalls = 0; + let upcomingCalls = 0; + server.use( + http.get('/api/auth/travel-stats', () => { statsCalls += 1; return HttpResponse.json({ totalTrips: 1 + statsCalls, totalDays: 10 * statsCalls }); }), + http.get('/api/reservations/upcoming', () => { + upcomingCalls += 1; + return HttpResponse.json({ reservations: [{ id: 5, trip_id: 101, title: 'Louvre', type: 'ticket', reservation_time: upcomingCalls > 1 ? '2026-07-03T10:00' : '2026-07-01T10:00' }] }); + }), + http.post('/api/trips', () => HttpResponse.json({ trip: buildTrip({ id: 200, title: 'Iceland' }) })), + http.post('/api/trips/:id/copy', () => HttpResponse.json({ trip: { ...PARIS, id: 300, title: 'Paris (copy)' } })), + http.put('/api/trips/:id', () => HttpResponse.json({ trip: { ...PARIS, start_date: '2026-07-03', end_date: '2026-07-12' } })), + ); + const { result } = await mountLoaded(); + await waitFor(() => expect(result.current.stats?.totalTrips).toBe(2)); + + await act(async () => { await result.current.handleCreate({ title: 'Iceland' }); }); + await waitFor(() => expect(result.current.stats?.totalTrips).toBe(3)); + + act(() => { result.current.setCopyTrip(PARIS); }); + await act(async () => { await result.current.confirmCopy(); }); + await waitFor(() => expect(result.current.stats?.totalTrips).toBe(4)); + + act(() => { result.current.setEditingTrip(PARIS); }); + await act(async () => { await result.current.handleUpdate({ title: 'Paris Adventure', start_date: '2026-07-03', end_date: '2026-07-12' }); }); + await waitFor(() => expect(result.current.stats?.totalTrips).toBe(5)); + expect(result.current.upcoming[0].reservation_time).toBe('2026-07-03T10:00'); + expect(upcomingCalls).toBe(4); + }); + + it('FE-HOOK-DASH-028: deleting, archiving and restoring a trip refresh the stats too (#2604)', async () => { + let statsCalls = 0; + server.use( + http.get('/api/auth/travel-stats', () => { statsCalls += 1; return HttpResponse.json({ totalTrips: statsCalls }); }), + http.delete('/api/trips/:id', () => HttpResponse.json({ success: true })), + http.put('/api/trips/:id', () => HttpResponse.json({ trip: { ...PARIS, is_archived: 1 } })), + ); + const { result } = await mountLoaded(); + await waitFor(() => expect(statsCalls).toBe(1)); + + act(() => { result.current.setDeleteTrip(TOKYO); }); + await act(async () => { await result.current.confirmDelete(); }); + await waitFor(() => expect(statsCalls).toBe(2)); + + await act(async () => { await result.current.handleArchive(101); }); + await waitFor(() => expect(statsCalls).toBe(3)); + + await act(async () => { await result.current.handleUnarchive(101); }); + await waitFor(() => expect(statsCalls).toBe(4)); + }); + + it('FE-HOOK-DASH-029: a failed mutation does not refetch the side data', async () => { + let statsCalls = 0; + server.use( + http.get('/api/auth/travel-stats', () => { statsCalls += 1; return HttpResponse.json({ totalTrips: statsCalls }); }), + http.post('/api/trips/:id/copy', () => HttpResponse.json({}, { status: 500 })), + ); + const { result } = await mountLoaded(); + await waitFor(() => expect(statsCalls).toBe(1)); + + act(() => { result.current.setCopyTrip(PARIS); }); + await act(async () => { await result.current.confirmCopy(); }); + + expect(statsCalls).toBe(1); + }); }); diff --git a/client/src/pages/dashboard/useDashboard.ts b/client/src/pages/dashboard/useDashboard.ts index b72df493e..66fb8e518 100644 --- a/client/src/pages/dashboard/useDashboard.ts +++ b/client/src/pages/dashboard/useDashboard.ts @@ -67,11 +67,15 @@ export function useDashboard() { useEffect(() => { void loadTrips() }, []) // Travel stats + upcoming reservations power the atlas row and the sidebar. - // Both are best-effort: a failure just leaves that section empty. - useEffect(() => { + // Both are best-effort: a failure just leaves that section empty. Every trip + // mutation below reloads them, so the counters and the booking dates don't + // lag behind a created, copied or redated trip until the next visit. #2604 + const loadSideData = () => { authApi.travelStats().then(setStats).catch(() => {}) reservationsApi.upcoming().then((r: { reservations: UpcomingReservation[] }) => setUpcoming(r.reservations || [])).catch(() => {}) - }, []) + } + + useEffect(() => { loadSideData() }, []) const loadTrips = async () => { setIsLoading(true) @@ -124,6 +128,7 @@ export function useDashboard() { const data = await tripsApi.create(tripData) setTrips(prev => sortTrips([data.trip, ...prev])) toast.success(t('dashboard.toast.created')) + loadSideData() return data } catch (err: unknown) { throw new Error(getApiErrorMessage(err, t('dashboard.toast.createError'))) @@ -136,6 +141,7 @@ export function useDashboard() { const data = await tripRepo.update(editingTrip.id, tripData) setTrips(prev => sortTrips(prev.map(t => t.id === editingTrip.id ? data.trip : t))) toast.success(t('dashboard.toast.updated')) + loadSideData() } catch (err: unknown) { throw new Error(getApiErrorMessage(err, t('dashboard.toast.updateError'))) } @@ -153,6 +159,7 @@ export function useDashboard() { setTrips(prev => prev.filter(t => t.id !== deleteTrip.id)) setArchivedTrips(prev => prev.filter(t => t.id !== deleteTrip.id)) toast.success(t('dashboard.toast.deleted')) + loadSideData() } catch (err: unknown) { toast.error(getApiErrorMessage(err, t('dashboard.toast.deleteError'))) } @@ -165,6 +172,7 @@ export function useDashboard() { setTrips(prev => prev.filter(t => t.id !== id)) setArchivedTrips(prev => sortTrips([data.trip, ...prev])) toast.success(t('dashboard.toast.archived')) + loadSideData() } catch (err: unknown) { toast.error(getApiErrorMessage(err, t('dashboard.toast.archiveError'))) } @@ -176,6 +184,7 @@ export function useDashboard() { setArchivedTrips(prev => prev.filter(t => t.id !== id)) setTrips(prev => sortTrips([data.trip, ...prev])) toast.success(t('dashboard.toast.restored')) + loadSideData() } catch (err: unknown) { toast.error(getApiErrorMessage(err, t('dashboard.toast.restoreError'))) } @@ -188,6 +197,7 @@ export function useDashboard() { const data = await tripsApi.copy(copyTrip.id, { title: `${copyTrip.title} (${t('dashboard.copySuffix')})` }) setTrips(prev => sortTrips([data.trip, ...prev])) toast.success(t('dashboard.toast.copied')) + loadSideData() } catch (err: unknown) { toast.error(getApiErrorMessage(err, t('dashboard.toast.copyError'))) }