Multi-user trip planning web app in a single Docker container. Mullvad-style token accounts, trip sharing via join codes, day-by-day calendar with typed entries (activity, hotel, travel, flight, rental car, immigration, note), multi-leg flight segments with bundled IATA airport dataset, Leaflet/OSM map with per-leg great-circle km (air vs ground), rough km-driven vs rental included-km comparison, cost splitting with settle-up suggestions, flip-clock departure countdown. Node 20 + Express + SQLite (WAL, additive migrations), vanilla JS SPA, 44 API tests.
132 lines
5.4 KiB
JavaScript
132 lines
5.4 KiB
JavaScript
// Rental-car details subsection used inside the day editor for rental entries,
|
|
// plus a compact one-line renderer for the entry list. Pickup/dropoff use
|
|
// plain-text location names only (they do not feed the route), so there is no
|
|
// geocode/lat-lng UI here. Kept separate so dayEditor.js stays small.
|
|
import { el } from '../dom.js';
|
|
import { formatDate } from '../format.js';
|
|
|
|
// createRentalDetails({ initial, entryDate, onChange }) ->
|
|
// { node, read(), load(rental) }
|
|
// read() -> { rental: {...} | null, pickupDate } or { error }.
|
|
export function createRentalDetails({ initial = null, entryDate, onChange = () => {} } = {}) {
|
|
const brandI = textInput('Brand (e.g. Toyota)', 60);
|
|
const modelI = textInput('Model (e.g. Yaris Cross)', 60);
|
|
const carTypeI = textInput('Type (e.g. SUV)', 40);
|
|
const bookingI = textInput('Booking ref', 60);
|
|
const includedI = el('input', { class: 'input', type: 'number', min: '0', step: '1', placeholder: 'e.g. 1500' });
|
|
|
|
const pDate = el('input', { class: 'input', type: 'date' });
|
|
const pTime = el('input', { class: 'input', type: 'time' });
|
|
const pLoc = textInput('Location (free text)', 120);
|
|
const dDate = el('input', { class: 'input', type: 'date' });
|
|
const dTime = el('input', { class: 'input', type: 'time' });
|
|
const dLoc = textInput('Location (free text)', 120);
|
|
|
|
for (const inp of [brandI, modelI, carTypeI, bookingI, includedI, pDate, pTime, pLoc, dDate, dTime, dLoc]) {
|
|
inp.addEventListener('input', onChange);
|
|
}
|
|
|
|
const node = el(
|
|
'div',
|
|
{ class: 'rental-details' },
|
|
el('div', { class: 'form-row' },
|
|
field('Brand', brandI), field('Model', modelI)),
|
|
el('div', { class: 'form-row' },
|
|
field('Car type', carTypeI), field('Booking ref', bookingI),
|
|
field('Included km', includedI)),
|
|
el('div', { class: 'rental-blocks' },
|
|
rentalBlock('Pickup', pDate, pTime, pLoc),
|
|
rentalBlock('Dropoff', dDate, dTime, dLoc)),
|
|
);
|
|
|
|
function read() {
|
|
const brand = brandI.value.trim();
|
|
const model = modelI.value.trim();
|
|
const car_type = carTypeI.value.trim();
|
|
const booking_ref = bookingI.value.trim();
|
|
const includedRaw = includedI.value.trim();
|
|
const pickupDate = pDate.value || entryDate;
|
|
const dropoffDate = dDate.value || pickupDate;
|
|
|
|
const anyContent = brand || model || car_type || booking_ref || includedRaw ||
|
|
pTime.value || pLoc.value.trim() || dTime.value || dLoc.value.trim();
|
|
if (!anyContent) return { rental: null, pickupDate };
|
|
|
|
let included_km = null;
|
|
if (includedRaw !== '') {
|
|
const n = Number(includedRaw);
|
|
if (!Number.isFinite(n) || n < 0) return { error: 'Included km must be a number ≥ 0.' };
|
|
included_km = n;
|
|
}
|
|
|
|
const rental = {};
|
|
if (brand) rental.brand = brand;
|
|
if (model) rental.model = model;
|
|
if (car_type) rental.car_type = car_type;
|
|
if (booking_ref) rental.booking_ref = booking_ref;
|
|
if (included_km != null) rental.included_km = included_km;
|
|
rental.pickup = { date: pickupDate };
|
|
if (pTime.value) rental.pickup.time = pTime.value;
|
|
if (pLoc.value.trim()) rental.pickup.location_name = pLoc.value.trim();
|
|
rental.dropoff = { date: dropoffDate };
|
|
if (dTime.value) rental.dropoff.time = dTime.value;
|
|
if (dLoc.value.trim()) rental.dropoff.location_name = dLoc.value.trim();
|
|
return { rental, pickupDate };
|
|
}
|
|
|
|
function load(rental) {
|
|
const r = rental || {};
|
|
brandI.value = r.brand || '';
|
|
modelI.value = r.model || '';
|
|
carTypeI.value = r.car_type || '';
|
|
bookingI.value = r.booking_ref || '';
|
|
includedI.value = r.included_km != null ? String(r.included_km) : '';
|
|
const p = r.pickup || {};
|
|
pDate.value = p.date || entryDate || '';
|
|
pTime.value = p.time || '';
|
|
pLoc.value = p.location_name || '';
|
|
const d = r.dropoff || {};
|
|
dDate.value = d.date || p.date || entryDate || '';
|
|
dTime.value = d.time || '';
|
|
dLoc.value = d.location_name || '';
|
|
onChange();
|
|
}
|
|
|
|
// Initial values (defaults: both dates to the entry's day).
|
|
load(initial || { pickup: { date: entryDate }, dropoff: { date: entryDate } });
|
|
|
|
return { node, read, load };
|
|
}
|
|
|
|
// "🚙 Toyota Yaris Cross · pickup 09:00 CNX Airport → dropoff 7 Aug 18:00 · 1,500 km included · RC-889231"
|
|
export function renderRentalLine(rental) {
|
|
const car = [rental.brand, rental.model].filter(Boolean).join(' ') || 'Rental car';
|
|
const p = rental.pickup || {};
|
|
const d = rental.dropoff || {};
|
|
const pickup = ['pickup', p.time, p.location_name].filter(Boolean).join(' ');
|
|
const dropoff = ['dropoff', d.date ? formatDate(d.date) : null, d.time, d.location_name].filter(Boolean).join(' ');
|
|
const bits = [`🚙 ${car}`, `${pickup} → ${dropoff}`];
|
|
if (rental.included_km != null) bits.push(`${Number(rental.included_km).toLocaleString()} km included`);
|
|
if (rental.booking_ref) bits.push(rental.booking_ref);
|
|
return el('div', { class: 'entry-rental muted' }, bits.join(' · '));
|
|
}
|
|
|
|
function textInput(placeholder, maxlength) {
|
|
return el('input', { class: 'input', type: 'text', maxlength: String(maxlength), placeholder });
|
|
}
|
|
|
|
function field(label, input) {
|
|
return el('label', { class: 'field field-grow' }, el('span', { class: 'field-label' }, label), input);
|
|
}
|
|
|
|
function rentalBlock(title, dateInput, timeInput, locInput) {
|
|
return el(
|
|
'div',
|
|
{ class: 'rental-block' },
|
|
el('h4', { class: 'rental-block-title' }, title),
|
|
el('div', { class: 'form-row' },
|
|
field('Date', dateInput), field('Time', timeInput)),
|
|
field('Location', locInput),
|
|
);
|
|
}
|