Rework entry types: merge hotel into stay, transport with modes, auto-transport

- Type set is now activity/stay/transport/flight/rental/note; hotel, travel
  and immigration are removed with idempotent startup data migrations
  (hotel->stay, travel->transport, immigration->activity with flag prefix)
- Transport entries carry an optional mode (train/bus/ferry/taxi/drive/other)
  that drives the chip/map icon; route stops expose transport_mode
- Creating a stay auto-creates a bridging transport to its neighbouring
  stays unless a transport/flight already covers the gap (one-shot)
- Summary: transports count replaces hotels/travelLegs
This commit is contained in:
2026-07-19 17:47:21 +07:00
parent b066e7b885
commit d393e16ae1
17 changed files with 469 additions and 62 deletions
+19 -7
View File
@@ -270,7 +270,7 @@ test('entry CRUD and full-row shape', async () => {
const entry = create.body.entry;
assert.deepEqual(
Object.keys(entry).sort(),
['date', 'end_date', 'details', 'end_time', 'id', 'lat', 'lng', 'location_name', 'paid_by', 'participants', 'price', 'rental', 'segments', 'sort_order', 'split_mode', 'start_time', 'title', 'trip_id', 'type'].sort()
['date', 'end_date', 'details', 'end_time', 'id', 'lat', 'lng', 'location_name', 'paid_by', 'participants', 'price', 'rental', 'segments', 'sort_order', 'split_mode', 'start_time', 'title', 'transport_mode', 'trip_id', 'type'].sort()
);
assert.equal(entry.end_date, null);
assert.equal(entry.details, '');
@@ -321,6 +321,17 @@ test('entry validation: type, title, lat/lng pairing and ranges', async () => {
assert.equal(ok.status, 201);
});
test('legacy entry types (hotel, travel, immigration) are rejected', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
const base = `/api/trips/${trip.id}/entries`;
for (const type of ['hotel', 'travel', 'immigration']) {
const res = await agent.post(base).send({ date: '2026-08-01', type, title: 'x' });
assert.equal(res.status, 400, `expected ${type} to be rejected`);
}
});
test('non-member cannot add or view entries', async () => {
const owner = await createAccount();
const guest = await createAccount();
@@ -348,13 +359,13 @@ test('route computes legs, totalKm and summary counts', async () => {
});
// Chiang Mai
await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', type: 'hotel', title: 'Hotel CNX', sort_order: 1,
date: '2026-08-01', type: 'stay', title: 'Hotel CNX', sort_order: 1,
location_name: 'Chiang Mai', lat: 18.7883, lng: 98.9853,
});
// A second flight + hotel + activities + a travel leg (no coords) to exercise counts
// A second flight + stay + activities + a transport leg (no coords) to exercise counts
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'flight', title: 'F2' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'hotel', title: 'H2' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'travel', title: 'Drive' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'stay', title: 'H2' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'transport', title: 'Drive' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-03', type: 'activity', title: 'A1' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-03', type: 'activity', title: 'A2' });
@@ -371,8 +382,9 @@ test('route computes legs, totalKm and summary counts', async () => {
assert.equal(s.days, 10);
assert.equal(s.nights, 9);
assert.equal(s.flights, 2);
assert.equal(s.hotels, 2);
assert.equal(s.travelLegs, 1);
assert.equal(s.transports, 1);
assert.equal(s.hotels, undefined);
assert.equal(s.travelLegs, undefined);
assert.equal(s.activities, 2);
assert.deepEqual(s.locations, ['Bangkok', 'Chiang Mai']);
});