Files
trip-plan/public/js/views/summary.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

112 lines
3.9 KiB
JavaScript

// Summary panel built from the /route response's `summary` block:
// days, nights, flights, hotels, travel legs, activities, total km, and the
// list of locations in visit order.
import { el } from '../dom.js';
export function renderSummary(tctx) {
const route = tctx.route || {};
const s = route.summary || {
days: 0, nights: 0, flights: 0, flightSegments: 0, hotels: 0, travelLegs: 0, activities: 0, locations: [],
};
const totalKm = route.totalKm || 0;
// Show the leg count under the Flights tile only when a flight has segments.
const flightSub = s.flightSegments > 0
? `${s.flightSegments} ${s.flightSegments === 1 ? 'leg' : 'legs'}`
: '';
const section = el('section', { class: 'card summary-section' });
section.appendChild(
el(
'div',
{ class: 'section-head' },
el('h2', {}, 'Summary'),
el('p', { class: 'muted' }, 'Trip at a glance.'),
),
);
const tiles = el(
'div',
{ class: 'stat-grid' },
tile('🗓️', s.days, s.days === 1 ? 'Day' : 'Days'),
tile('🌙', s.nights, s.nights === 1 ? 'Night' : 'Nights'),
tile('✈️', s.flights, 'Flights', flightSub),
tile('🏨', s.hotels, 'Hotels'),
tile('🚗', s.travelLegs, 'Travel legs'),
tile('📍', s.activities, 'Activities'),
s.rentals > 0 ? tile('🚙', s.rentals, s.rentals === 1 ? 'Rental' : 'Rentals') : null,
);
section.appendChild(tiles);
const kmDriven = s.kmDriven || 0;
const kmAir = s.kmAir || 0;
const includedKm = s.includedKm != null ? s.includedKm : null;
const kmBlock = el(
'div',
{ class: 'km-block' },
el(
'div',
{ class: 'total-km' },
el('span', { class: 'total-km-value' }, fmtKm(totalKm)),
el('span', { class: 'total-km-label' }, 'km total (great-circle)'),
),
);
const breakdown = el('div', { class: 'km-breakdown' });
if (kmDriven > 0) {
// Rental allowance (includedKm) is paired with the rough driven figure;
// flag red when the rough estimate already exceeds the included allowance.
const over = includedKm != null && kmDriven > includedKm;
const drivenText = includedKm != null
? `≈ ${fmtKm(kmDriven)} km / ${fmtKm(includedKm)} incl.`
: `≈ ${fmtKm(kmDriven)} km`;
breakdown.appendChild(kmRow('🚗', drivenText, 'driven',
over
? 'Rough great-circle estimate already exceeds the included allowance — real road km will be higher'
: 'Rough great-circle estimate — not routed driving distance',
over ? 'km-over' : ''));
}
if (kmAir > 0) {
breakdown.appendChild(kmRow('✈️', `${fmtKm(kmAir)} km`, 'flown',
'Great-circle distance between airports'));
}
if (breakdown.childElementCount) kmBlock.appendChild(breakdown);
section.appendChild(kmBlock);
const locations = s.locations || [];
const locBlock = el('div', { class: 'loc-block' }, el('h3', {}, 'Locations in order'));
if (!locations.length) {
locBlock.appendChild(el('p', { class: 'muted' }, 'No located stops yet.'));
} else {
const ol = el('ol', { class: 'loc-order' });
for (const name of locations) ol.appendChild(el('li', {}, name));
locBlock.appendChild(ol);
}
section.appendChild(locBlock);
return section;
}
function kmRow(icon, valueText, label, title, cls = '') {
return el(
'div',
{ class: `km-row${cls ? ` ${cls}` : ''}`, title },
el('span', { class: 'km-row-icon' }, icon),
el('span', { class: 'km-row-value' }, valueText),
el('span', { class: 'km-row-label muted' }, label),
);
}
function tile(icon, value, label, sub) {
return el(
'div',
{ class: 'stat-tile' },
el('span', { class: 'stat-icon' }, icon),
el('span', { class: 'stat-value' }, String(value ?? 0)),
el('span', { class: 'stat-label' }, label),
sub ? el('span', { class: 'stat-sub' }, sub) : null,
);
}
function fmtKm(n) {
return (Math.round((Number(n) || 0) * 10) / 10).toLocaleString();
}