# T12 — Trip rename / delete / merge **Phase** 4 · **Depends on** T10, T11 · **Status** Done ## Goal Let rides be renamed, deleted, and merged. Done when a ride can be given a meaningful name, junk test rides can be removed, and two trips can be combined into one with correct recomputed aggregates. ## Context Merge exists as a **safety valve**. It was originally promised to rescue a ride wrongly split by the migration heuristic; that heuristic is gone now that v1 data is being discarded, but merge keeps earning its place: - A ride accidentally stopped and restarted mid-route - Test rides that should be one logical trip - Any future automatic splitting (v3+) needs an undo Delete is the mundane but most-used of the three — a session of testing leaves a dozen 30-second trips cluttering the list. `Trip.name` is already nullable in the T02 schema, with the UI deriving a date label when it is null. CASCADE foreign keys mean delete is a single statement. ## State after T08–T11 `TripDetailViewModel` already has `rename(name)` and `delete(onDeleted)`; `TripsViewModel` has `delete(tripId)`. The repository has `renameTrip` (which already collapses blank to null) and `deleteTrip`. What is missing is the **merge** operation and all of the UI: selection mode on the list, the rename affordance on detail, and the confirmation dialogs. `ConfirmDialog` exists in `ui/components/Stats.kt`. ## Design ### Rename Inline edit on the detail header. Empty input clears back to null (date-derived label) rather than storing an empty string — those two states must not diverge. ### Delete From both the list (swipe or overflow) and detail. Confirmation stating what is lost. CASCADE removes segments and points. After deleting from detail, navigate back. ### Merge Selected from the trips list: long-press to enter selection mode, pick exactly two, then Merge. Semantics: - The **earlier** trip (by `startedAt`) is the survivor - The later trip's segments are re-parented to the survivor - **Segments are never joined** — the boundary between the two rides becomes a segment boundary, exactly like a pause. This is correct: the rider genuinely was not recording in between, and joining them would draw a false straight line across the gap - `endedAt` becomes the later trip's `endedAt` - The now-empty later trip row is deleted - Aggregates are **recomputed from scratch** with `RideStatistics.compute()`, not summed - The survivor keeps its name if set; otherwise inherits the later trip's Merge runs in a single `@Transaction` — a partial merge would leave orphaned segments. Merging is not offered while either trip is active. ## Implementation 1. `TripRepository.renameTrip(id, name: String?)` 2. `TripRepository.deleteTrip(id)` — CASCADE 3. `TripRepository.mergeTrips(survivorId, absorbedId)` in one transaction: re-parent segments → update `endedAt` → delete absorbed row → recompute aggregates 4. Selection mode in `TripsScreen`, enabled at exactly two selections 5. Confirmation dialogs for delete and merge; merge states the resulting distance 6. Inline rename on the detail header ## Acceptance criteria - [x] Rename persists; clearing reverts to the date label - [x] Delete removes the trip and every point (verified by row count) - [x] Merge re-parents all segments and deletes the absorbed row - [x] Merged aggregates equal a fresh computation over the combined points - [x] Merged trip shows a segment boundary at the join, not a joined line - [x] Merge unavailable unless exactly two completed trips are selected - [x] Merge is atomic — an interrupted merge leaves no orphans ## Tests Instrumented: - Delete cascades: zero segments, zero points remain - Merge: segment count is the sum of both; point count is the sum; no orphans - Merged aggregates match `RideStatistics.compute()` over combined points - Merge of trips out of chronological order still picks the earlier as survivor - Rename to empty stores null, not `""` Compose UI: selection mode enables Merge only at two selections. **Not written** — the UI-test debt from T08–T11 covers this too; tracked in T18. ## Risks / gotchas - **Do not sum aggregates on merge.** Distance is not additive across the join — there is a real gap. Recompute. - **Chronological survivor.** Selection order is not ride order; sort by `startedAt`. - **Deleting the active trip** must go through the service's discard path (T06), not a raw delete, or the service keeps writing into a deleted trip. ## Out of scope Splitting a trip; bulk delete; undo.