fix(dashboard): trip counters and upcoming bookings refresh after a trip is created, copied, redated, archived or deleted

The travel stats and the upcoming bookings were only fetched when the
dashboard mounted, so "Trips" and "Days" kept their old values after a new
or duplicated trip and the sidebar still listed booking dates from before a
date change until the page was left. Every successful trip mutation now
reloads both, on desktop and mobile alike since they share the hook.

Refs #2604
This commit is contained in:
Maurice
2026-10-12 20:20:27 +02:00
parent fee41ab394
commit 93cce57306
2 changed files with 78 additions and 3 deletions
@@ -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);
});
});
+13 -3
View File
@@ -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')))
}