fix(tours): simplify post-save confirmation

This commit is contained in:
elmocito
2026-10-06 13:44:08 +02:00
parent 411a6052fe
commit 99f3bbfb09
3 changed files with 34 additions and 31 deletions
@@ -146,16 +146,18 @@ describe('TourPlannerToursRail', () => {
expect(controller.openTour).toHaveBeenCalledWith(tour)
})
it('TOUR-PLANNER-RAIL-003: assigns the saved tour immediately when a day is picked', async () => {
it('keeps the post-save confirmation compact and assigns through the saved TOUR-PLANNER row action', async () => {
const controller = planner({ saveOutcome: tour, editingPlaceId: tour.place_id })
const onAssignToDay = vi.fn().mockResolvedValue(undefined)
render(<TourPlannerToursRail planner={controller} tours={[tour]} {...railProps} onAssignToDay={onAssignToDay} />)
expect(screen.getByRole('status')).toHaveTextContent('Tour saved')
expect(screen.queryByRole('button', { name: 'Assign to a day' })).not.toBeInTheDocument()
expect(onAssignToDay).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('button', { name: 'Plan another' }))
expect(controller.startNewTour).toHaveBeenCalledOnce()
expect(controller.discard).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('button', { name: 'Assign to a day' }))
fireEvent.click(screen.getByRole('button', { name: 'Add to day: Saved ridge walk' }))
const dayOption = screen.getByRole('button', { name: /Summit day/ })
expect(dayOption).toHaveTextContent(/Day 1/i)
expect(dayOption).toHaveTextContent(/May 15/i)
@@ -164,16 +166,17 @@ describe('TourPlannerToursRail', () => {
await waitFor(() => expect(onAssignToDay).toHaveBeenCalledWith(42, 7))
})
it('keeps the saved outcome and its day action when assignment fails', async () => {
it('keeps the save confirmation and row assignment action available when row assignment fails', async () => {
const controller = planner({ saveOutcome: tour, editingPlaceId: tour.place_id })
const onAssignToDay = vi.fn().mockResolvedValue(false)
render(<TourPlannerToursRail planner={controller} tours={[tour]} {...railProps} onAssignToDay={onAssignToDay} />)
fireEvent.click(screen.getByRole('button', { name: 'Assign to a day' }))
fireEvent.click(screen.getByRole('button', { name: 'Add to day: Saved ridge walk' }))
fireEvent.click(screen.getByRole('button', { name: /Summit day/ }))
await waitFor(() => expect(onAssignToDay).toHaveBeenCalledWith(42, 7))
expect(screen.getByRole('status')).toHaveTextContent('Tour saved')
expect(screen.getByRole('button', { name: 'Assign to a day' })).toBeEnabled()
expect(screen.queryByRole('button', { name: 'Assign to a day' })).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Add to day: Saved ridge walk' })).toBeEnabled()
})
it('TOUR-PLANNER-RAIL-004: protects a dirty draft before selecting a GPX Tour', () => {
@@ -398,7 +398,6 @@ export function TourPlannerToursRail({ planner, tours, days, loading, onAssignTo
const toast = useToast()
const distanceUnit = useSettingsStore(state => state.settings.distance_unit)
const [pendingTour, setPendingTour] = useState<TourListItem | null>(null)
const [assigning, setAssigning] = useState(false)
const isSaving = planner.isSaving
const hasRoute = (planner.mode.type === 'new-draft' || planner.mode.type === 'edit-saved') && planner.route !== null
const routeAnalysis = planner.routeAnalysis
@@ -422,17 +421,6 @@ export function TourPlannerToursRail({ planner, tours, days, loading, onAssignTo
selectTour(tour)
}
const assignSavedTour = async (dayId: number) => {
const savedTour = planner.saveOutcome
if (!canAssign || !savedTour) return
setAssigning(true)
try {
await onAssignToDay(savedTour.place_id, dayId)
} finally {
setAssigning(false)
}
}
const walkingTime = { label: t('tours.planner.inspector.duration'), value: t('tours.durationMinutes', { count: Math.max(1, Math.round((planner.durationSeconds ?? 0) / 60)) }) }
return (
@@ -464,20 +452,9 @@ export function TourPlannerToursRail({ planner, tours, days, loading, onAssignTo
<CheckCircle2 size={16} strokeWidth={2} className="flex-none text-success" />
{t('tours.planner.saved')}
</div>
<div className="flex flex-wrap gap-2">
<DialogButton onClick={() => { if (canEdit && !isSaving) planner.startNewTour() }} disabled={!canEdit || isSaving}>
{t('tours.planner.planAnother')}
</DialogButton>
<TourDayMenu
variant="button"
primary
days={days}
placeId={planner.saveOutcome.place_id}
label={t('tours.planner.assignToDay')}
disabled={!canAssign || assigning}
onPick={day => { void assignSavedTour(day.id) }}
/>
</div>
<DialogButton onClick={() => { if (canEdit && !isSaving) planner.startNewTour() }} disabled={!canEdit || isSaving}>
{t('tours.planner.planAnother')}
</DialogButton>
</div>
)}
@@ -1101,4 +1101,27 @@ describe('useTourPlanner', () => {
await act(async () => { expect(await result.current.save()).toBeNull() })
expect(updateTour).not.toHaveBeenCalled()
})
it('keeps the existing save-failure notice and draft when a Tour update fails', async () => {
detailTour.mockResolvedValue({
tour: savedTour,
waypoints: [
{ lat: 48, lng: 11, role: 'start', sequence: 0 },
{ lat: 48.01, lng: 11.02, role: 'end', sequence: 1 },
],
})
updateTour.mockRejectedValue(new Error('Tour update failed'))
const { result } = renderHook(() => useTourPlanner({ tripId: 29 }))
await act(async () => { expect(await result.current.openTour(savedTour)).toBe(true) })
act(() => result.current.setName('Keep this draft'))
await act(async () => { await vi.advanceTimersByTimeAsync(450) })
const view = render(<TourPlannerRail planner={result.current} />)
await act(async () => { expect(await result.current.save()).toBeNull() })
view.rerender(<TourPlannerRail planner={result.current} />)
expect(result.current.editingPlaceId).toBe(savedTour.place_id)
expect(result.current.name).toBe('Keep this draft')
expect(screen.getByText('Tour could not be saved. Your draft is still here.')).toBeInTheDocument()
})
})