mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(accommodations): catch the nights the upgrade itself could not reach
A migration runs while the old container is still answering. A booking written in those seconds comes from code that knows nothing about the day stop and lands behind the sweep that would have given it one: on the test instance exactly one of ten, and invisible to the drive ever after, with no way back short of saving it again. The sweep runs a second time, which is safe because the same NOT EXISTS decides it both times. The other way into that state is closed too. "Remove from day" in the place panel took the stop and left the booking, which is the very state this whole change exists to remove — and it was the only control that could produce it after the upgrade. A stop a booking wrote is no longer offered there, in either direction: removing it strands the booking, adding one puts the same hotel on the day twice. The night is removed where it is made, in the day's overnight block or by turning it back into a pause in road trip mode.
This commit is contained in:
@@ -1478,4 +1478,24 @@ describe('PlaceInspector', () => {
|
|||||||
expect(screen.getByText('Museum Ticket').closest('[role="button"]')).toHaveAttribute('data-no-press');
|
expect(screen.getByText('Museum Ticket').closest('[role="button"]')).toHaveAttribute('data-no-press');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
|
||||||
|
it('FE-PLANNER-INSPECTOR-017b: a stop a booked night wrote is neither removed nor doubled here', () => {
|
||||||
|
// Taking it off the day would leave the booking behind with nothing on the drive and
|
||||||
|
// no way back short of saving it again; offering to add it would put a second copy
|
||||||
|
// of the same hotel beside it. The night is removed where it is made: the day's
|
||||||
|
// overnight block, or turning it back into a pause in road trip mode.
|
||||||
|
const onRemoveAssignment = vi.fn();
|
||||||
|
const booked = [{ id: 99, place, day_id: 1, place_id: place.id, order_index: 0, notes: null, accommodation_id: 4 }];
|
||||||
|
render(
|
||||||
|
<PlaceInspector
|
||||||
|
{...defaultProps}
|
||||||
|
selectedDayId={1}
|
||||||
|
assignments={{ '1': booked } as never}
|
||||||
|
onRemoveAssignment={onRemoveAssignment}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByRole('button', { name: /remove from day/i })).toBeNull();
|
||||||
|
expect(screen.queryByRole('button', { name: /add to day/i })).toBeNull();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|||||||
@@ -321,6 +321,8 @@ export default function PlaceInspector({
|
|||||||
? ((selectedAssignmentId ? dayAssignments.find(a => a.id === selectedAssignmentId) : null)
|
? ((selectedAssignmentId ? dayAssignments.find(a => a.id === selectedAssignmentId) : null)
|
||||||
?? dayAssignments.find(a => a.place?.id === place.id))
|
?? dayAssignments.find(a => a.place?.id === place.id))
|
||||||
: null
|
: null
|
||||||
|
/** This stop belongs to a booked night rather than to the traveller. */
|
||||||
|
const bookedNight = assignmentInDay?.accommodation_id != null
|
||||||
|
|
||||||
// The weekday lines are display text; the ring is computed from the structured
|
// The weekday lines are display text; the ring is computed from the structured
|
||||||
// periods next to them, in the place's own timezone. open_now stays the fallback.
|
// periods next to them, in the place's own timezone. open_now stays the fallback.
|
||||||
@@ -509,8 +511,14 @@ export default function PlaceInspector({
|
|||||||
{mode === 'collection' && collectionStatus && onSetStatus && (
|
{mode === 'collection' && collectionStatus && onSetStatus && (
|
||||||
<StatusBadge status={collectionStatus} onChange={onSetStatus} t={t} />
|
<StatusBadge status={collectionStatus} onChange={onSetStatus} t={t} />
|
||||||
)}
|
)}
|
||||||
{/* Trip mode — day assignment */}
|
{/* Trip mode — day assignment.
|
||||||
{mode === 'trip' && !!selectedDayId && (
|
A stop a lodging booking put there is not offered either way. Taking it off
|
||||||
|
the day would leave the booking behind with nothing on the drive and no way
|
||||||
|
back short of saving it again, and putting a second one beside it is the
|
||||||
|
duplicate the stop exists to prevent. The booking is removed where it is
|
||||||
|
made: in the day's overnight block, or by turning the night back into a
|
||||||
|
pause in road trip mode. */}
|
||||||
|
{mode === 'trip' && !!selectedDayId && !bookedNight && (
|
||||||
assignmentInDay ? (
|
assignmentInDay ? (
|
||||||
<ActionButton onClick={() => onRemoveAssignment?.(selectedDayId, assignmentInDay.id)} variant="ghost" icon={<Minus size={13} />}
|
<ActionButton onClick={() => onRemoveAssignment?.(selectedDayId, assignmentInDay.id)} variant="ghost" icon={<Minus size={13} />}
|
||||||
label={<span className="hidden sm:inline">{t('inspector.removeFromDay')}</span>} />
|
label={<span className="hidden sm:inline">{t('inspector.removeFromDay')}</span>} />
|
||||||
|
|||||||
@@ -4782,6 +4782,45 @@ function runMigrations(db: Database.Database): void {
|
|||||||
}
|
}
|
||||||
if (stays.length > 0) console.log(`[DB] Put ${stays.length} booked night(s) on their check-in day`);
|
if (stays.length > 0) console.log(`[DB] Put ${stays.length} booked night(s) on their check-in day`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The same sweep once more, for the nights the first one could not have seen.
|
||||||
|
*
|
||||||
|
* A migration runs while the old container is still answering: a booking written
|
||||||
|
* in those seconds is written by code that knows nothing about the day stop, and
|
||||||
|
* lands behind the sweep that would have given it one. One did, on the test
|
||||||
|
* instance, out of ten. There is nothing to be done about that window, but there
|
||||||
|
* is something to be done about what falls into it, and a booking that never got
|
||||||
|
* its stop is invisible to the drive with no way back short of saving it again.
|
||||||
|
*
|
||||||
|
* Safe to repeat: the same NOT EXISTS decides it, so a stay that already has its
|
||||||
|
* stop is passed over, and one whose place the traveller planned by hand keeps
|
||||||
|
* that row unclaimed. Every future release can carry the same step for the same
|
||||||
|
* reason.
|
||||||
|
*/
|
||||||
|
() => {
|
||||||
|
const stays = db.prepare(`
|
||||||
|
SELECT a.id, a.place_id, a.start_day_id
|
||||||
|
FROM day_accommodations a
|
||||||
|
WHERE a.place_id IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM day_assignments da
|
||||||
|
WHERE da.day_id = a.start_day_id AND da.place_id = a.place_id
|
||||||
|
)
|
||||||
|
ORDER BY a.id
|
||||||
|
`).all() as Array<{ id: number; place_id: number; start_day_id: number }>;
|
||||||
|
|
||||||
|
const insert = db.prepare(
|
||||||
|
`INSERT INTO day_assignments (day_id, place_id, order_index, accommodation_id)
|
||||||
|
VALUES (?, ?, COALESCE((SELECT MAX(order_index) + 1 FROM day_assignments WHERE day_id = ?), 0), ?)`
|
||||||
|
);
|
||||||
|
const stamp = db.prepare("UPDATE places SET stop_type = 'hotel' WHERE id = ? AND (stop_type IS NULL OR stop_type = '')");
|
||||||
|
for (const stay of stays) {
|
||||||
|
insert.run(stay.start_day_id, stay.place_id, stay.start_day_id, stay.id);
|
||||||
|
stamp.run(stay.place_id);
|
||||||
|
}
|
||||||
|
if (stays.length > 0) console.log(`[DB] Caught up ${stays.length} booked night(s) missed during the upgrade`);
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (currentVersion < migrations.length) {
|
if (currentVersion < migrations.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user