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:
2026-08-30 12:49:17 +02:00
parent f272e74b84
commit b0bdd2570c
10 changed files with 725 additions and 87 deletions
+23
View File
@@ -405,3 +405,26 @@ Proxies `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=5&accept
- Session cookie is httpOnly; frontend detects auth state via `GET /api/auth/me` on load.
- Expenses UI lives in `public/js/views/expenses.js` (+ `public/css/expenses.css` — styles.css is at its 500-line cap), rendered as a card in the trip detail side column directly below Costs (above Checklist). Self-fetching from `GET .../expenses` (never triggers a whole-trip refresh; after add/edit/delete it re-fetches itself AND tells the Costs panel to refresh). Shows: trip total + per-day grouped rows (day heading with day total; each row = category icon, description, payer, amount) by default; a **sort control** (Date ↑/↓, Amount ↑/↓, Category, Payer — non-date sorts flatten to a single list, pure client-side); a quick-add row (date defaulting to today clamped into the trip range, description, amount, category select, payer, split — same split modes/participants UI pattern as `costForm.js`); edit + delete per row; and an **Export CSV** button that simply navigates to `GET .../expenses/export.csv` (cookie auth makes a plain link work).
- Checklist UI lives in `public/js/views/checklist.js`, rendered as a card in the trip detail side column (below Costs). It shows a progress bar, items grouped by category with a checkbox / qty / 🔒-personal marker per row, inline add, drag-reorder (`dragdrop.js` `enableReorder`, desktop-only like the rest), an "Uncheck all" action, and a "💡 Suggestions" modal listing the advice with per-item checkboxes and "Add selected". Ticking a box PATCHes optimistically and re-syncs on failure.
### Hourly day timeline & "now" indicator (frontend-only — no API surface)
Pure client feature over existing entry fields (`start_time`/`end_time` `HH:MM`, `date`/`end_date`). No new endpoints, no schema changes.
**Hourly day timeline** — when a day holds **4 or more** entries (all entries whose `date` equals the opened day, stays included), the day-editor slideover (`dayEditor.js`) renders an hour-grid timeline between the header and the entry list. Days with 13 entries are unchanged. Implementation is split to respect the 500-line cap (dayEditor is at 488):
- `public/js/timelineLayout.js` (new) — **pure, DOM-free** helpers, imported by the view AND unit-tested directly from node:
- `timedSlots(entries)` → entries with a valid `start_time`, each as `{entry, startMin, endMin}` (minutes since midnight; missing/invalid `end_time``endMin = startMin + 60`; `end_time``start_time` (or spanning into `end_date`) clamps to `24*60`). Invalid `start_time` (not `HH:MM`) counts as untimed.
- `layoutColumns(slots)` → same slots + `{col, cols}` per slot: overlapping slots split the row into side-by-side columns (greedy: sort by `startMin` then `endMin`; place each slot in the first column whose last slot ends ≤ its start; `cols` = column count of its overlap cluster).
- `gridWindow(slots)``{startHour, endHour}`: one full hour before the earliest `startMin` to one full hour after the latest `endMin`, clamped to `[0, 24]`; `{startHour: 8, endHour: 21}` when `slots` is empty.
- `public/js/views/dayTimeline.js` (new) — `renderDayTimeline(tctx, date, dayEntries)` → a DOM node (or `null` below the threshold, so dayEditor can just append the result):
- **All-day strip** on top: entries with no valid `start_time` (plus stays regardless of time) as compact type-coloured chips, icon + title.
- **Hour grid** below (only when ≥1 timed slot): one row per hour of `gridWindow`, `HH:00` labels left, timed entries as absolutely positioned blocks (top/height from start/end minutes, ≥ 30 min visual height; overlapping blocks share the width per `layoutColumns`). Blocks show icon + title (+ time range in the `title` tooltip), coloured via the entry type's `--chip` var like everywhere else.
- Clicking an all-day chip or a block loads that entry into the day editor's edit form (same handler as clicking ✎ in the list).
- The timeline re-renders with the rest of the panel on `tctx._onModalRefresh` — no self-fetching, it reads the already-loaded `tctx.trip.entries`.
- `public/css/timeline.css` (new, linked in `index.html`) — all styles for the strip, grid, and both now-markers below (styles.css is at 450/500 — do not grow it).
**Red "now" indicator** — both views, only while "now" is actually inside what they show; updates every minute (`setInterval` 60 s, cleared when the view unmounts — calendar re-render or slideover close):
- **Month grid** (`calendar.js`): today's `.cal-day` cell gets a vertical red line whose horizontal position within the cell = the fraction of the day elapsed (00:00 left edge → 24:00 right edge), with a small dot at the top — the "slider" that moves through the cell over the day. Rendered only when today is inside the rendered week rows. The cell also gets a `cal-today` class (subtle red day-number accent) so today is findable at a glance.
- **Hourly timeline** (`dayTimeline.js`): when the opened `date` is today and the current time falls inside the grid window, a horizontal red line with a left dot across the grid at the current-time position (the classic Google-Calendar line).
- One shared colour token for both (`--now`, red), defined in `timeline.css`.