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
+22 -3
View File
@@ -16,6 +16,7 @@ import {
isMultiDay,
} from '../format.js';
import { computeStayLayout, renderWeekBands } from './stayBands.js';
import { makeChipDraggable, makeDayDropTarget } from './dragdrop.js';
const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
@@ -54,6 +55,8 @@ export function renderCalendar(tctx) {
}
const stayLayout = computeStayLayout(entries);
// id -> entry, for the drop handler to resolve the dragged chip.
const entriesById = new Map(entries.map((e) => [e.id, e]));
const rangeStart = parseYMD(trip.start_date);
const rangeEnd = parseYMD(trip.end_date);
@@ -97,17 +100,23 @@ export function renderCalendar(tctx) {
if (bands) weekEl.appendChild(bands);
const daysRow = el('div', { class: 'cal-week-days' });
for (const date of week) {
daysRow.appendChild(dayCell(date, rangeStart, rangeEnd, byDate, contByDate, dropoffByDate, tctx, currency));
daysRow.appendChild(dayCell(date, rangeStart, rangeEnd, byDate, contByDate, dropoffByDate, entriesById, tctx, currency));
}
weekEl.appendChild(daysRow);
cal.appendChild(weekEl);
}
// A cancelled drag (released outside a cell) fires dragend on the source chip,
// which bubbles up here — sweep any lingering drop highlight.
cal.addEventListener('dragend', () => {
for (const n of cal.querySelectorAll('.cal-day-drop')) n.classList.remove('cal-day-drop');
});
section.appendChild(cal);
return section;
}
function dayCell(date, rangeStart, rangeEnd, byDate, contByDate, dropoffByDate, tctx, currency) {
function dayCell(date, rangeStart, rangeEnd, byDate, contByDate, dropoffByDate, entriesById, tctx, currency) {
const key = ymd(date);
const inRange = date >= rangeStart && date <= rangeEnd;
// Stays render as bands, not chips, so exclude them from the day cell.
@@ -158,6 +167,12 @@ function dayCell(date, rangeStart, rangeEnd, byDate, contByDate, dropoffByDate,
});
}
// Only in-range days accept dropped entries; new sort_order appends after the
// day's current entries (byDate holds every entry, incl. stays, for that day).
if (inRange) {
makeDayDropTarget(cell, key, (byDate.get(key) || []).length, entriesById, tctx);
}
return cell;
}
@@ -167,7 +182,7 @@ function chip(entry, currency) {
const chain = hasSegments(entry) ? flightChain(entry.segments) : '';
const car = hasRental(entry) ? [entry.rental.brand, entry.rental.model].filter(Boolean).join(' ') : '';
const label = chain || car || entry.title;
return el(
const node = el(
'div',
{
class: 'cal-chip',
@@ -180,6 +195,10 @@ function chip(entry, currency) {
? el('span', { class: 'chip-price' }, formatMoney(entry.price, currency, { compact: true }))
: null,
);
// Regular chips are drag sources (move the entry to another day). The
// "…continues" and "dropoff" ghost chips and stay bands stay non-draggable.
makeChipDraggable(node, entry);
return node;
}
// Ghost chip on the days a multi-day entry spans after its start. Clicking