mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user