Add multi-day entries and area stay blocks

Entries gain an optional inclusive end_date (backfilled via migration): flights and travel longer than 24h show a continuation marker on following days, and a new stay entry type marks a time frame in one area (e.g. 3 days Venice) rendered as continuous bands across the calendar weeks. Stays feed the map route as stops; the route summary gains stays count and a chronological areas list with day spans. 49 API tests.
This commit is contained in:
2026-07-19 00:30:14 +07:00
parent 1d86c68665
commit 154f56a0a0
14 changed files with 416 additions and 26 deletions
+24 -3
View File
@@ -12,6 +12,7 @@ const ENTRY_TYPES = new Set([
'hotel',
'activity',
'rental',
'stay',
'note',
]);
const SPLIT_MODES = new Set(['equal', 'own', 'payer']);
@@ -44,6 +45,25 @@ function validateEntry(body, { partial, existing, memberIds }) {
}
fields.date = body.date;
}
// end_date: optional inclusive end, null clears, must be >= the (effective) date.
// ISO YYYY-MM-DD strings compare correctly lexicographically.
const effDate = 'date' in fields ? fields.date : existing?.date;
if (has('end_date')) {
if (body.end_date === null) {
fields.end_date = null;
} else {
if (!isValidDateStr(body.end_date)) {
return { error: 'end_date must be a valid YYYY-MM-DD date' };
}
if (effDate && body.end_date < effDate) {
return { error: 'end_date must be on or after date' };
}
fields.end_date = body.end_date;
}
} else if (partial && 'date' in fields && existing?.end_date && existing.end_date < fields.date) {
// Moving date past a stored end_date would invert the range.
return { error: 'end_date would be before the new date; clear or update end_date' };
}
if (!partial || has('title')) {
if (typeof body.title !== 'string' || body.title.trim() === '') {
return { error: 'title is required' };
@@ -190,7 +210,7 @@ export default function entriesRoutes(db) {
`SELECT ${ENTRY_COLUMNS} FROM entries WHERE trip_id = ? ORDER BY date, sort_order, id`
);
const getEntryRow = db.prepare(
'SELECT trip_id, split_mode, paid_by, type FROM entries WHERE id = ?'
'SELECT trip_id, split_mode, paid_by, type, date, end_date FROM entries WHERE id = ?'
);
const getMemberIds = db.prepare('SELECT user_id FROM trip_members WHERE trip_id = ?');
const insertParticipant = db.prepare(
@@ -235,13 +255,14 @@ export default function entriesRoutes(db) {
const info = db
.prepare(
`INSERT INTO entries
(trip_id, date, type, title, details, start_time, end_time,
(trip_id, date, end_date, type, title, details, start_time, end_time,
location_name, lat, lng, sort_order, price, paid_by, split_mode, segments, rental)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(
tripId,
f.date,
f.end_date ?? null,
f.type,
f.title,
f.details ?? '',