feat(accommodations): put a booked night on its check-in day

A road-trip stop is a day assignment: the rail, the server-side plan and the
map all build their stops from day_assignments and only look the stay up
afterwards, to hang check-in and check-out on one. A stay written without an
assignment was therefore invisible to every routing surface, so a hotel entered
in the day planner never reached the road-trip view and had to be entered a
second time as an ordinary place. The road-trip side has always written both
halves in one go; this is the day planner catching up, in the domain that
writes the stay.

Booking a night now also assigns its place to the check-in day, inside the
transaction that already writes the stay and its partner hotel reservation, and
types the place as a hotel stop when it carries no type of its own, so it reads
exactly like a night booked in the road trip. Only the check-in day gets a
stop; the later nights ride on the stay row. A place already planned for that
day gets no second stop and the booking claims none, so cancelling it cannot
delete a stop somebody placed by hand. Moving the booking moves its own stop,
deleting it takes its own stop back, and a trip re-date carries it along with
the stay. All four write surfaces (REST, plugin RPC and both MCP tools) run the
same write and announce the same events; the answers carry the stop too,
because the broadcast skips the socket that sent the request.

New nullable day_assignments.accommodation_id marks a stop as a booking's own.
It stays NULL for every existing row on purpose: everything planned before this
was planned by hand, and no backfill rewrites a finished trip. A stay booked
earlier picks up its stop the next time somebody saves it.

The booking form keeps writing stays on its own: that surface is gated on
reservation_edit and the day stop is day_edit work.
This commit is contained in:
Maurice
2026-09-13 01:13:25 +02:00
parent 3ab5ddaacc
commit f0e1da2d81
19 changed files with 628 additions and 83 deletions
+11
View File
@@ -4731,6 +4731,17 @@ function runMigrations(db: Database.Database): void {
const hasSource = db.prepare("SELECT 1 FROM pragma_table_info('places') WHERE name = 'source'").get();
if (!hasSource) db.exec('ALTER TABLE places ADD COLUMN source TEXT');
},
// Provenance for a day stop that a lodging booking put there rather than the
// traveller: it carries the stay's id, so moving or deleting the booking can
// move or delete exactly that stop and never one somebody placed by hand.
// Deliberately left NULL for every row that already exists: everything planned
// before this shipped was planned by hand, and a booking must not start
// claiming ownership of it.
() => {
const hasColumn = db.prepare("SELECT 1 FROM pragma_table_info('day_assignments') WHERE name = 'accommodation_id'").get();
if (!hasColumn) db.exec('ALTER TABLE day_assignments ADD COLUMN accommodation_id INTEGER');
},
];
if (currentVersion < migrations.length) {