fix(roadtrip): the hotel at the edge of a day stands, drives and reads right

A hotel row now takes its position and name from the trip's place instead of the stay
row, which the planner fetches again only when a stay is edited. A moved pin no longer
seats the hotel at its old spot while the server plans from the new one. The drive out
of the morning hotel goes the way the first place is reached from it, the mode Days
draws that leg in, and it is set in the shared rule so the browser and
calculate_roadtrip agree.

On the rail, a hotel row that took in the morning marker starts the line. A day that
only drives from one stay to the next shows no stop count and takes a dropped stop as
its first. The hotel keeps its pin on the desktop map, and the switch explains itself
under its label and to screen readers. On the phone, the note about figures set on the
desktop now sits above the switch it does not cover. The hint and the refusal on the
hotel's drive say what they mean in all 23 languages. The via tools and the wiki say
that a via into a booked night is kept but not used, the wiki states the overlap rule
Days applies, and the arrived-from branch of the via anchor has tests.
This commit is contained in:
Maurice
2026-09-23 15:29:44 +02:00
parent 05651eed1a
commit e6e520b78f
46 changed files with 547 additions and 111 deletions
@@ -21,7 +21,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
function hourlyRouter() {
return {
profiles: () => ['driving'],
route: vi.fn(async (_user: number, _trip: number, _day: number, points: { lat: number; lng: number }[]) => ({
route: vi.fn(async (_user: number, _trip: number, _day: number, points: { lat: number; lng: number }[], _profile?: string) => ({
parts: points.slice(1).map(() => ({ distance: 60000, duration: 3600 })),
avoidMissed: [],
leg: {
@@ -512,6 +512,23 @@ describe('a booked night at both ends of its days', () => {
expect(lats(router)[1]).toEqual([HOTEL.lat, 45.3, 45.4, HOTEL.lat]);
});
it('drives out of the morning hotel the way the first place is reached from it, as the browser and the day plan do', async () => {
const { user, trip: created, router, plans } = simeon();
db.prepare(
"UPDATE day_assignments SET incoming_leg_transport_mode = 'walking' WHERE place_id = (SELECT id FROM places WHERE trip_id = ? AND name = 'P3')",
).run(created.id);
const { calculated } = await plans.calculate(created.id, user.id);
expect(calculated.days[1].stops[0]).toMatchObject({ legMode: 'walking', bookend: { phase: 'morning' } });
const runs = router.route.mock.calls.map((call) => [call[4], call[3].map((p: { lat: number }) => p.lat)]);
// The hotel's leg on foot on its own, the rest of the day in the day's own mode.
expect(runs).toContainEqual(['walking', [HOTEL.lat, 45.3]]);
expect(runs).toContainEqual(['driving', [45.3, 45.4, HOTEL.lat]]);
// The other mornings are reached no particular way and stay in one run.
expect(runs).toContainEqual(['driving', [HOTEL.lat, 45.5, 45.6, HOTEL.lat]]);
});
it('drives the stored days while the trip has it off, and a preview can switch it either way', async () => {
const off = simeon({});
const stored = await off.plans.calculate(off.trip.id, off.user.id);