feat: undo button for trip planner

Implements a full undo history system for the Plan screen.

New hook: usePlannerHistory (client/src/hooks/usePlannerHistory.ts)
- Maintains a LIFO stack (up to 30 entries) of reversible actions
- Exposes pushUndo(label, fn), undo(), canUndo, lastActionLabel

Tracked actions:
- Assign place to day (undo: remove the assignment)
- Remove place from day (undo: re-assign at original position)
- Reorder places within a day (undo: restore previous order)
- Move place to a different day (undo: move back)
- Optimize route (undo: restore original order)
- Lock / unlock place (undo: toggle back)
- Delete place (undo: recreate place + restore all day assignments)
- Add place (undo: delete it)
- Import from GPX (undo: delete all imported places)
- Import from Google Maps list (undo: delete all imported places)

UI: Undo button (Undo2 icon) in DayPlanSidebar header. PDF, ICS and
Undo buttons all use custom instant hover tooltips instead of native
title attributes.

A toast notification confirms each undo action.

Translations: undo.* keys added to all 12 language files.
This commit is contained in:
Luca
2026-04-01 18:20:14 +02:00
parent 411d5408c1
commit e308204808
16 changed files with 440 additions and 65 deletions

View File

@@ -0,0 +1,29 @@
import { useRef, useReducer } from 'react'
export interface UndoEntry {
label: string
undo: () => Promise<void> | void
}
export function usePlannerHistory(maxEntries = 30) {
const historyRef = useRef<UndoEntry[]>([])
const [, forceUpdate] = useReducer((x: number) => x + 1, 0)
const pushUndo = (label: string, undoFn: () => Promise<void> | void) => {
historyRef.current = [{ label, undo: undoFn }, ...historyRef.current].slice(0, maxEntries)
forceUpdate()
}
const undo = async () => {
if (historyRef.current.length === 0) return
const [first, ...rest] = historyRef.current
historyRef.current = rest
forceUpdate()
try { await first.undo() } catch (e) { console.error('Undo failed:', e) }
}
const canUndo = historyRef.current.length > 0
const lastActionLabel = historyRef.current[0]?.label ?? null
return { pushUndo, undo, canUndo, lastActionLabel }
}