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:
@@ -9,12 +9,15 @@ import {
|
||||
ENTRY_TYPE_LIST,
|
||||
splitModeLabel,
|
||||
typeInfo,
|
||||
formatDate,
|
||||
formatFullDate,
|
||||
formatTimeRange,
|
||||
formatMoney,
|
||||
flightChain,
|
||||
hasSegments,
|
||||
hasRental,
|
||||
isMultiDay,
|
||||
entrySpanDays,
|
||||
} from '../format.js';
|
||||
import { createFlightRoute, renderSegmentLines } from './segments.js';
|
||||
import { createRentalDetails, renderRentalLine } from './rental.js';
|
||||
@@ -119,6 +122,7 @@ export function openDayEditor(tctx, date) {
|
||||
flight ? ` · ✈️ ${flightChain(entry.segments)}` : '',
|
||||
!flight && entry.location_name ? ` · 📍 ${entry.location_name}` : '',
|
||||
),
|
||||
isMultiDay(entry) ? el('div', { class: 'entry-span muted' }, spanText(entry)) : null,
|
||||
flight ? renderSegmentLines(entry.segments) : null,
|
||||
hasRental(entry) ? renderRentalLine(entry.rental) : null,
|
||||
hasPrice
|
||||
@@ -140,6 +144,16 @@ export function openDayEditor(tctx, date) {
|
||||
);
|
||||
}
|
||||
|
||||
// "until 8 Aug · 3 days" for stays; "5 Aug 20:50 → 7 Aug 06:30" otherwise.
|
||||
function spanText(entry) {
|
||||
if (entry.type === 'stay') {
|
||||
return `until ${formatDate(entry.end_date)} · ${entrySpanDays(entry)} days`;
|
||||
}
|
||||
const a = `${formatDate(entry.date)}${entry.start_time ? ` ${entry.start_time}` : ''}`;
|
||||
const b = `${formatDate(entry.end_date)}${entry.end_time ? ` ${entry.end_time}` : ''}`;
|
||||
return `${a} → ${b}`;
|
||||
}
|
||||
|
||||
function startEdit(entry) {
|
||||
editing = entry.id;
|
||||
loc = entry.lat != null && entry.lng != null
|
||||
@@ -152,6 +166,7 @@ export function openDayEditor(tctx, date) {
|
||||
fields.details.value = entry.details || '';
|
||||
fields.start.value = entry.start_time || '';
|
||||
fields.end.value = entry.end_time || '';
|
||||
fields.endDate.value = entry.end_date || '';
|
||||
fields.cost.prefill(entry);
|
||||
// Flight segments (load() triggers the flight-route onChange -> UI sync).
|
||||
fields.flightRoute.load(Array.isArray(entry.segments) ? entry.segments : []);
|
||||
@@ -174,6 +189,11 @@ export function openDayEditor(tctx, date) {
|
||||
const detailsInput = el('textarea', { class: 'input', rows: '2', placeholder: 'Details (optional)' });
|
||||
const startInput = el('input', { class: 'input', type: 'time' });
|
||||
const endInput = el('input', { class: 'input', type: 'time' });
|
||||
// Optional end date for multi-day entries (all types). Relabeled "Until"
|
||||
// for stays via syncTypeUI.
|
||||
const endDateInput = el('input', { class: 'input', type: 'date', min: date });
|
||||
const endDateLabel = el('span', { class: 'field-label' }, 'End date');
|
||||
const endDateField = el('label', { class: 'field' }, endDateLabel, endDateInput);
|
||||
|
||||
const locWrap = el('div', { class: 'loc-field' });
|
||||
const locInput = el('input', {
|
||||
@@ -218,6 +238,9 @@ export function openDayEditor(tctx, date) {
|
||||
flightSection.style.display = type === 'flight' ? '' : 'none';
|
||||
rentalSection.style.display = type === 'rental' ? '' : 'none';
|
||||
locationField.style.display = type === 'flight' && flightRoute.hasSegments() ? 'none' : '';
|
||||
// Stays emphasise an end date ("until"); other types call it "End date".
|
||||
endDateLabel.textContent = type === 'stay' ? 'Until' : 'End date';
|
||||
locationField.classList.toggle('field-emphasis', type === 'stay');
|
||||
}
|
||||
typeSelect.addEventListener('change', syncTypeUI);
|
||||
typeUIReady = true;
|
||||
@@ -228,6 +251,7 @@ export function openDayEditor(tctx, date) {
|
||||
|
||||
fields = {
|
||||
type: typeSelect, title: titleInput, details: detailsInput, start: startInput, end: endInput,
|
||||
endDate: endDateInput,
|
||||
locInput, locResults, locSelected,
|
||||
cost: costForm, flightRoute, rentalDetails,
|
||||
};
|
||||
@@ -240,11 +264,19 @@ export function openDayEditor(tctx, date) {
|
||||
async function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
errorEl.textContent = '';
|
||||
const title = titleInput.value.trim();
|
||||
if (!title) return (errorEl.textContent = 'Title is required.');
|
||||
const isStay = typeSelect.value === 'stay';
|
||||
let title = titleInput.value.trim();
|
||||
if (!title) {
|
||||
// Stays may leave the title blank — default to the location's short name.
|
||||
if (isStay && loc) title = loc.name.split(',')[0].trim();
|
||||
else return (errorEl.textContent = isStay
|
||||
? 'Give the stay a title or pick a location.'
|
||||
: 'Title is required.');
|
||||
}
|
||||
|
||||
const payload = {
|
||||
date,
|
||||
end_date: endDateInput.value || null,
|
||||
type: typeSelect.value,
|
||||
title,
|
||||
details: detailsInput.value.trim(),
|
||||
@@ -286,6 +318,12 @@ export function openDayEditor(tctx, date) {
|
||||
payload.rental = null;
|
||||
}
|
||||
|
||||
// end_date must be on/after the effective start date (rental may have
|
||||
// moved payload.date to the pickup date above).
|
||||
if (payload.end_date && payload.end_date < payload.date) {
|
||||
return (errorEl.textContent = 'End date must be on or after the start date.');
|
||||
}
|
||||
|
||||
// Cost fields: price is the toggle. When set, send the full cost set;
|
||||
// when blank, send price:null (clears any prior cost) and omit the rest.
|
||||
const costRes = costForm.read();
|
||||
@@ -337,6 +375,7 @@ export function openDayEditor(tctx, date) {
|
||||
{ class: 'form-row' },
|
||||
el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Start time'), startInput),
|
||||
el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'End time'), endInput),
|
||||
endDateField,
|
||||
),
|
||||
flightSection,
|
||||
rentalSection,
|
||||
|
||||
Reference in New Issue
Block a user