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:
@@ -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 ?? '',
|
||||
|
||||
Reference in New Issue
Block a user