diff --git a/server/src/nest/assignments/assignments.service.ts b/server/src/nest/assignments/assignments.service.ts index 2f4e97cbc..5e534aba3 100644 --- a/server/src/nest/assignments/assignments.service.ts +++ b/server/src/nest/assignments/assignments.service.ts @@ -212,10 +212,16 @@ export class AssignmentsService { if (placeTime) { const assignment = this.dbs.get<{ day_id: number }>('SELECT day_id FROM day_assignments WHERE id = ?', id); if (assignment) { + // A booked night's hour lives on the booking, not on the stop: nobody types a + // time into a hotel row, they type a check-in. Left out of this, the night + // counted as untimed and stayed wherever it had been dropped, so pinning an + // afternoon stop sorted that one and left the hotel sitting in front of or + // behind it by accident. const dayAssignments = this.dbs.all<{ id: number; effective_time: string | null }>(` - SELECT da.id, COALESCE(da.assignment_time, p.place_time) as effective_time + SELECT da.id, COALESCE(da.assignment_time, p.place_time, acc.check_in) as effective_time FROM day_assignments da JOIN places p ON da.place_id = p.id + LEFT JOIN day_accommodations acc ON acc.id = da.accommodation_id WHERE da.day_id = ? ORDER BY da.order_index ASC `, assignment.day_id); diff --git a/server/tests/e2e/assignments.e2e.test.ts b/server/tests/e2e/assignments.e2e.test.ts index 7a93fcdb0..25c089e02 100644 --- a/server/tests/e2e/assignments.e2e.test.ts +++ b/server/tests/e2e/assignments.e2e.test.ts @@ -34,7 +34,13 @@ const { db } = vi.hoisted(() => { tmp.exec(`CREATE TABLE day_assignments (id INTEGER PRIMARY KEY AUTOINCREMENT, day_id INTEGER NOT NULL, place_id INTEGER NOT NULL, order_index INTEGER NOT NULL DEFAULT 0, notes TEXT, assignment_time TEXT, assignment_end_time TEXT, leg_transport_mode TEXT, + accommodation_id INTEGER, created_at TEXT DEFAULT (datetime('now')));`); + // The auto-sort reads a booked night's hour off its booking, so the table has to be + // here even though nothing in this file books one. + tmp.exec(`CREATE TABLE day_accommodations (id INTEGER PRIMARY KEY AUTOINCREMENT, trip_id INTEGER, + place_id INTEGER, start_day_id INTEGER, end_day_id INTEGER, check_in TEXT, check_in_end TEXT, + check_out TEXT, confirmation TEXT, notes TEXT, created_at TEXT DEFAULT (datetime('now')));`); tmp.exec(`CREATE TABLE assignment_participants (assignment_id INTEGER NOT NULL, user_id INTEGER NOT NULL, UNIQUE(assignment_id, user_id));`); tmp.exec(`CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, color TEXT, created_at TEXT);`); diff --git a/server/tests/unit/nest/assignments.service.test.ts b/server/tests/unit/nest/assignments.service.test.ts index 8f3f5767f..3c97ffc0e 100644 --- a/server/tests/unit/nest/assignments.service.test.ts +++ b/server/tests/unit/nest/assignments.service.test.ts @@ -369,6 +369,25 @@ describe('updateTime', () => { expect([order(a.id), order(c.id), order(b.id)]).toEqual([0, 1, 2]); }); + it('ASG-SVC-022b: a booked night sorts by its check-in, which is where its hour lives', () => { + // Nobody types a time into a hotel row, they type a check-in, and it sits on the + // booking. Counted as untimed, the night stayed wherever it had been dropped: pin an + // afternoon stop and the hotel it was booked around ended up behind it. + const { trip, day, place } = fixture(); + const hotel = createPlace(testDb, trip.id, { name: 'Billstedt' }); + const afternoon = createDayAssignment(testDb, day.id, place.id, { order_index: 0 }); + const night = createDayAssignment(testDb, day.id, hotel.id, { order_index: 1 }); + const stay = testDb.prepare( + "INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in) VALUES (?, ?, ?, ?, '11:00')" + ).run(trip.id, hotel.id, day.id, day.id); + testDb.prepare('UPDATE day_assignments SET accommodation_id = ? WHERE id = ?').run(Number(stay.lastInsertRowid), night.id); + + svc.updateTime(afternoon.id, '16:00', null); + + const order = (id: number) => (testDb.prepare('SELECT order_index FROM day_assignments WHERE id = ?').get(id) as { order_index: number }).order_index; + expect(order(night.id)).toBeLessThan(order(afternoon.id)); + }); + it('ASG-SVC-023: clearing with null persists but skips the auto-sort (falsy gate)', () => { const { day, place } = fixture(); const a = createDayAssignment(testDb, day.id, place.id, { order_index: 0 }); diff --git a/shared/src/roadtrip/assemble.ts b/shared/src/roadtrip/assemble.ts index a012cd973..edafa5895 100644 --- a/shared/src/roadtrip/assemble.ts +++ b/shared/src/roadtrip/assemble.ts @@ -17,7 +17,6 @@ import type { } from './planning-types'; import { computeSchedule, - parseClock, deriveDriveWarnings, refuelsRange, isServiceStopType, @@ -159,7 +158,7 @@ export function assembleRoadtrip({ legs.reduce((sum, l) => sum + (l?.duration ?? 0), 0) + inbound.reduce((sum, l) => sum + (l?.duration ?? 0), 0); const schedule = chain.schedule; const legVias = routed.map((l) => l?.vias ?? []); - const stops = chain.stops.map((s, index) => { + const stops = chain.stops.map((s) => { const snap = s.automaticNight ? undefined : allSnaps[stopKey(s)]; const line = spurFor(snap); if (line) accessLines.push({ line, meters: snap!.meters, stopKey: stopKey(s) }); diff --git a/shared/src/roadtrip/continueSchedule.ts b/shared/src/roadtrip/continueSchedule.ts index 15f40ebd2..485150aad 100644 --- a/shared/src/roadtrip/continueSchedule.ts +++ b/shared/src/roadtrip/continueSchedule.ts @@ -5,7 +5,9 @@ export function continueSchedule( stops: RoadtripStop[], schedule: Schedule, legs: (number | undefined)[], - dayNumber: number, + /** Unused since a check-out stopped shaping the chain; kept so the call sites and + * their specs read the same as every other scheduler entry point. */ + _dayNumber?: number, ): Schedule { const entries = schedule.entries.map((entry) => ({ ...entry })); const warnings = [...schedule.warnings];