Add drag & drop for entries and per-stay band colours

Calendar chips can be dragged onto another in-range day (multi-day entries shift end_date by the same delta in one PATCH); day-editor rows get a drag handle to reorder within a day, patching sort_order only for changed positions. Continues/dropoff ghost chips and stay bands are not draggable; touch devices fall back to the existing click/edit flow. Each stay band now gets a deterministic colour from an 8-hue palette by chronological index, mirrored as dots in the summary Areas list.
This commit is contained in:
2026-07-19 02:09:26 +07:00
parent 2eda9e4f6d
commit b066e7b885
6 changed files with 225 additions and 11 deletions
+19 -3
View File
@@ -22,6 +22,7 @@ import {
import { createFlightRoute, renderSegmentLines } from './segments.js';
import { createRentalDetails, renderRentalLine } from './rental.js';
import { createCostForm } from './costForm.js';
import { enableRowReorder } from './dragdrop.js';
export function openDayEditor(tctx, date) {
// A form-state object for the entry currently being added/edited.
@@ -87,21 +88,35 @@ export function openDayEditor(tctx, date) {
if (!dayEntries.length) {
list.appendChild(el('p', { class: 'muted entry-empty' }, 'Nothing planned for this day yet.'));
} else {
for (const entry of dayEntries) list.appendChild(entryRow(entry));
// Reordering only makes sense with 2+ entries — then each row gets a drag
// handle and dropping recomputes sort_order (see enableRowReorder).
const reorderable = dayEntries.length > 1;
const rows = [];
for (const entry of dayEntries) {
const { node, handle } = entryRow(entry, reorderable);
rows.push({ node, handle, entry });
list.appendChild(node);
}
if (reorderable) enableRowReorder(rows, () => tctx.refreshTrip());
}
mount(panel, header, list, formSection(dayEntries));
panel.scrollTop = 0;
}
function entryRow(entry) {
// Returns { node, handle } — handle is the drag grip (null unless reorderable).
function entryRow(entry, reorderable) {
const info = typeInfo(entry.type);
const time = formatTimeRange(entry.start_time, entry.end_time);
const hasPrice = entry.price != null;
const flight = hasSegments(entry);
return el(
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',
@@ -142,6 +157,7 @@ export function openDayEditor(tctx, date) {
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.