Files
trip-plan/public/js/views/rental.js
T
grabowski fe89bb2b1c Initial release: collaborative trip planner
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.
2026-07-18 23:15:29 +07:00

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),
);
}