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:
+17
-7
@@ -45,13 +45,23 @@ entries (id INTEGER PK, trip_id INTEGER NOT NULL REFERENCES trips(id),
|
||||
paid_by INTEGER REFERENCES users(id), -- who paid (null = unassigned)
|
||||
split_mode TEXT NOT NULL DEFAULT 'equal', -- 'equal' | 'own' | 'payer'
|
||||
segments TEXT, -- JSON array, flight entries only (see below)
|
||||
rental TEXT, -- JSON object, rental entries only (see below)
|
||||
transport_mode TEXT, -- transport entries only (see below)
|
||||
created_at TEXT DEFAULT current_timestamp)
|
||||
entry_participants (entry_id INTEGER REFERENCES entries(id), user_id INTEGER REFERENCES users(id),
|
||||
PRIMARY KEY (entry_id, user_id))
|
||||
-- no rows for an entry = "all trip members participate" (dynamic default)
|
||||
```
|
||||
|
||||
Entry `type` ∈ `flight | immigration | travel | hotel | activity | rental | stay | note`.
|
||||
Entry `type` ∈ `flight | transport | activity | rental | stay | note`.
|
||||
|
||||
**Legacy types** (removed 2026-07): `hotel`, `travel`, `immigration`. Idempotent data migrations run at startup: `hotel` rows become `stay`, `travel` rows become `transport`, `immigration` rows become `activity` with the title prefixed `🛂 `. POST/PATCH with a legacy type is a 400 (`invalid entry type`).
|
||||
|
||||
### Transport entries
|
||||
|
||||
`transport` covers ground/sea travel (the old `travel` type). Optional `transport_mode` ∈ `train | bus | ferry | taxi | drive | other` (nullable; drives the icon in the UI). Validation: only allowed when the effective `type === 'transport'` (else 400 `transport_mode is only allowed on transport entries`); PATCH `transport_mode: null` clears it. Entry JSON always includes `transport_mode` (string or null).
|
||||
|
||||
**Auto-transport between stays**: when a `stay` entry is POSTed and it has a chronological neighbour stay (the nearest stay before and/or after it, ordered by `date`), the server auto-creates one `transport` entry per neighbour pair — titled `"<from short name> → <to short name>"` (short name = first comma-segment of the stay's `location_name`, fallback `title`), dated on the **later** stay's `date`, `transport_mode` null, no price/location, `sort_order` 0. Skipped when any `transport` or `flight` entry already exists with `date` between the earlier stay's end (`end_date` or `date`) and the later stay's `date` (inclusive). One-shot: fires only on stay **creation** (never PATCH), so deleting an auto-created transport does not resurrect it. The POST response is unchanged (`201 {entry}` = the stay); clients should refetch the entry list.
|
||||
|
||||
### Multi-day entries & stays
|
||||
|
||||
@@ -155,11 +165,11 @@ User JSON shape everywhere: `{id, display_name}`.
|
||||
|
||||
| Method & path | Body | Response |
|
||||
|---|---|---|
|
||||
| `POST /api/trips/:id/entries` | `{date, end_date?, type, title, details?, start_time?, end_time?, location_name?, lat?, lng?, sort_order?, price?, paid_by?, split_mode?, participants?, segments?}` | `201 {entry}`; validates type enum, date format, end_date null or valid date ≥ date, title non-empty ≤200 chars; lat/lng must both be present or both absent, lat ∈ [-90,90], lng ∈ [-180,180]; price null or number ≥ 0; paid_by null or a trip member's user id; split_mode ∈ `equal\|own\|payer` (`payer` requires paid_by); participants null/[] (= all members) or array of trip-member user ids; segments per "Flight segments" above |
|
||||
| `POST /api/trips/:id/entries` | `{date, end_date?, type, title, details?, start_time?, end_time?, location_name?, lat?, lng?, sort_order?, price?, paid_by?, split_mode?, participants?, segments?}` | `201 {entry}`; validates type enum, date format, end_date null or valid date ≥ date, title non-empty ≤200 chars; lat/lng must both be present or both absent, lat ∈ [-90,90], lng ∈ [-180,180]; price null or number ≥ 0; paid_by null or a trip member's user id; split_mode ∈ `equal\|own\|payer` (`payer` requires paid_by); participants null/[] (= all members) or array of trip-member user ids; segments per "Flight segments" above; transport_mode per "Transport entries" above (POSTing a stay may auto-create a transport entry, see same section) |
|
||||
| `PATCH /api/entries/:id` | any subset of the above | `200 {entry}` (member of the entry's trip required; `participants` replaces the whole set) |
|
||||
| `DELETE /api/entries/:id` | — | `204` (also deletes its entry_participants rows) |
|
||||
|
||||
Entry JSON shape (always full row): `{id, trip_id, date, end_date, type, title, details, start_time, end_time, location_name, lat, lng, sort_order, price, paid_by, split_mode, participants, segments, rental}` where `participants` is an array of user ids (`[]` = all members) and `segments` is the parsed array or `null`.
|
||||
Entry JSON shape (always full row): `{id, trip_id, date, end_date, type, title, details, start_time, end_time, location_name, lat, lng, sort_order, price, paid_by, split_mode, participants, segments, rental, transport_mode}` where `participants` is an array of user ids (`[]` = all members) and `segments` is the parsed array or `null`.
|
||||
|
||||
### Route & summary (computed)
|
||||
|
||||
@@ -173,7 +183,7 @@ Entry JSON shape (always full row): `{id, trip_id, date, end_date, type, title,
|
||||
"totalKm": 587.3,
|
||||
"summary": {
|
||||
"days": 10, "nights": 9,
|
||||
"flights": 2, "flightSegments": 4, "hotels": 3, "travelLegs": 1, "activities": 4,
|
||||
"flights": 2, "flightSegments": 4, "transports": 1, "activities": 4,
|
||||
"rentals": 1, "includedKm": 1500,
|
||||
"stays": 2, "areas": [{"name": "Venice", "days": 3}, {"name": "Berlin", "days": 7}],
|
||||
"kmAir": 0, "kmDriven": 587.3,
|
||||
@@ -182,9 +192,9 @@ Entry JSON shape (always full row): `{id, trip_id, date, end_date, type, title,
|
||||
}
|
||||
```
|
||||
|
||||
- `stops` = entries having lat/lng, ordered by `(date, sort_order, id)`; flight entries with coordinate-bearing segments are expanded into airport stops instead (see "Flight segments" — `kind: "airport"`, includes `code`).
|
||||
- `stops` = entries having lat/lng, ordered by `(date, sort_order, id)`; flight entries with coordinate-bearing segments are expanded into airport stops instead (see "Flight segments" — `kind: "airport"`, includes `code`). Stops also carry `transport_mode` (the entry's value, or null) so the map can show mode-specific icons.
|
||||
- `legs` = consecutive stop pairs; skip zero-distance pairs (< 0.05 km) — still include the stop, just no leg.
|
||||
- Every leg has `mode`: `"air"` when BOTH endpoints are `kind:"airport"` stops expanded from the SAME flight entry (i.e. actual flight segments); `"ground"` for everything else (hotel→airport transfers, city-to-city drives). `summary.kmAir` / `summary.kmDriven` are the per-mode sums (1 decimal). `kmDriven` is the rough rental-car figure: great-circle, so real road km will be somewhat higher.
|
||||
- Every leg has `mode`: `"air"` when BOTH endpoints are `kind:"airport"` stops expanded from the SAME flight entry (i.e. actual flight segments); `"ground"` for everything else (stay→airport transfers, city-to-city drives). `summary.kmAir` / `summary.kmDriven` are the per-mode sums (1 decimal). `kmDriven` is the rough rental-car figure: great-circle, so real road km will be somewhat higher.
|
||||
- `days` = inclusive count from start_date to end_date; `nights = days - 1` (0 for single-day trips).
|
||||
- `locations` = unique `location_name` values in stop order.
|
||||
- km rounded to 1 decimal.
|
||||
@@ -197,7 +207,7 @@ Entry JSON shape (always full row): `{id, trip_id, date, end_date, type, title,
|
||||
{
|
||||
"currency": "USD",
|
||||
"totalCost": 1450.0,
|
||||
"byType": { "flight": 800.0, "travel": 300.0, "hotel": 350.0 },
|
||||
"byType": { "flight": 800.0, "transport": 300.0, "stay": 350.0 },
|
||||
"perUser": [
|
||||
{ "userId": 1, "displayName": "brave-otter", "share": 725.0, "paid": 950.0, "net": 225.0 },
|
||||
{ "userId": 2, "displayName": "calm-heron", "share": 725.0, "paid": 500.0, "net": -225.0 }
|
||||
|
||||
Reference in New Issue
Block a user