Add hourly day timeline and red now indicators to the calendar
Days with 4+ entries open with an hour-grid timeline in the day editor: untimed entries in an all-day strip, timed ones positioned by start/end with side-by-side columns for overlaps, click-to-edit. Layout math lives in a pure, unit-tested module (timelineLayout.js). Today's month-grid cell gets a vertical red slider positioned by the fraction of the day elapsed; the timeline gets the classic horizontal red current-time line. Both tick every minute with cleanup on unmount, and the initial positioning runs unconditionally while the node is still detached (the isConnected self-heal gates only interval ticks). Also splits entryRow rendering out of dayEditor.js to stay under the 500-line cap.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// A single entry's read-only row in the day editor's list: icon, title,
|
||||
// price, sub-line (type/time/flight-chain/location), span text for multi-day
|
||||
// entries, flight-segment/rental detail lines, cost line, and edit/delete
|
||||
// actions. Split out of dayEditor.js to keep it under the repo's 500-line cap.
|
||||
import { el } from '../dom.js';
|
||||
import {
|
||||
typeInfo,
|
||||
formatDate,
|
||||
formatTimeRange,
|
||||
formatMoney,
|
||||
flightChain,
|
||||
hasSegments,
|
||||
hasRental,
|
||||
isMultiDay,
|
||||
entrySpanDays,
|
||||
splitModeLabel,
|
||||
} from '../format.js';
|
||||
import { renderSegmentLines } from './segments.js';
|
||||
import { renderRentalLine } from './rental.js';
|
||||
|
||||
// Returns { node, handle } — handle is the drag grip (null unless reorderable).
|
||||
export function renderEntryRow(entry, { reorderable, currency, memberName, onEdit, onDelete }) {
|
||||
const info = typeInfo(entry.type);
|
||||
const time = formatTimeRange(entry.start_time, entry.end_time);
|
||||
const hasPrice = entry.price != null;
|
||||
const flight = hasSegments(entry);
|
||||
const handle = reorderable
|
||||
? el('span', { class: 'entry-drag-handle', title: 'Drag to reorder', 'aria-hidden': 'true' }, '⋮⋮')
|
||||
: null;
|
||||
const node = el(
|
||||
'div',
|
||||
{ class: 'entry-row', style: { '--chip': info.color } },
|
||||
handle,
|
||||
el('span', { class: 'entry-icon' }, info.icon),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'entry-body' },
|
||||
el(
|
||||
'div',
|
||||
{ class: 'entry-title-row' },
|
||||
el('span', { class: 'entry-title' }, entry.title),
|
||||
hasPrice
|
||||
? el('span', { class: 'entry-price' }, formatMoney(entry.price, currency, { compact: true }))
|
||||
: null,
|
||||
),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'entry-sub muted' },
|
||||
info.label,
|
||||
time ? ` · ${time}` : '',
|
||||
flight ? ` · ✈️ ${flightChain(entry.segments)}` : '',
|
||||
!flight && entry.location_name ? ` · 📍 ${entry.location_name}` : '',
|
||||
entry.auto_ref ? ' · ↻ auto' : '',
|
||||
),
|
||||
isMultiDay(entry) ? el('div', { class: 'entry-span muted' }, spanText(entry)) : null,
|
||||
flight ? renderSegmentLines(entry.segments) : null,
|
||||
hasRental(entry) ? renderRentalLine(entry.rental) : null,
|
||||
hasPrice
|
||||
? el(
|
||||
'div',
|
||||
{ class: 'entry-cost muted' },
|
||||
`💰 ${splitModeLabel(entry.split_mode)}`,
|
||||
entry.paid_by != null ? ` · paid by ${memberName(entry.paid_by)}` : ' · no payer set',
|
||||
)
|
||||
: null,
|
||||
entry.details ? el('div', { class: 'entry-details' }, entry.details) : null,
|
||||
),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'entry-actions' },
|
||||
el('button', { class: 'icon-btn', title: 'Edit', onClick: () => onEdit(entry) }, '✎'),
|
||||
el('button', { class: 'icon-btn danger', title: 'Delete', onClick: () => onDelete(entry) }, '🗑'),
|
||||
),
|
||||
);
|
||||
return { node, handle };
|
||||
}
|
||||
|
||||
// "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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user