Add transport sync button and allow dropping stays onto band-covered days

- Auto-created transports carry auto_ref {from,to} stay ids; new
  POST /api/trips/:id/transports/regenerate reconciles them against the
  current stay order (re-date/re-title kept bridges preserving mode and
  price, delete orphans, create missing) without touching manual
  transports or flight-covered gaps
- Sync transports button in the calendar header with result toast
- The week band strip is now a drop target resolving the day from the
  pointer position, so stays can be dropped onto spots covered by other
  stays; overlapping stays stack in band lanes
This commit is contained in:
2026-07-20 10:22:49 +07:00
parent 71a158aa51
commit 36c4e4f306
14 changed files with 425 additions and 38 deletions
+50 -17
View File
@@ -46,10 +46,29 @@ export function makeBandDraggable(node, entry) {
node.addEventListener('dragend', () => node.classList.remove('dragging'));
}
// Wire an in-range day cell as a drop target. On drop the dragged entry moves to
// `targetDate` with sort_order = `targetCount` (appended after that day's
// entries); an entry with an end_date has it shifted by the same day-delta in
// the SAME patch (the backend rejects date > stored end_date otherwise).
// Shared move-commit: PATCHes the dragged entry to `targetDate` with the given
// `sortOrder`, shifting `end_date` by the same day-delta in the SAME patch (the
// backend rejects date > stored end_date otherwise). No-ops when dropped back
// on its own day. Used by both the day-cell and week-bands drop targets below.
async function commitEntryMove(id, targetDate, sortOrder, entriesById, tctx) {
const entry = entriesById.get(id);
if (!entry || entry.date === targetDate) return; // dropped on its own day
const patch = { date: targetDate, sort_order: sortOrder };
if (entry.end_date) {
const delta = dayDelta(entry.date, targetDate);
patch.end_date = ymd(addDays(parseYMD(entry.end_date), delta));
}
try {
await api.entries.update(id, patch);
await tctx.refreshTrip();
} catch (err) {
toast(err.message);
}
}
// Wire an in-range day cell as a drop target. On drop the dragged entry moves
// to `targetDate` with sort_order = `targetCount` (appended after that day's
// entries).
export function makeDayDropTarget(cell, targetDate, targetCount, entriesById, tctx) {
cell.addEventListener('dragover', (e) => {
e.preventDefault();
@@ -64,19 +83,33 @@ export function makeDayDropTarget(cell, targetDate, targetCount, entriesById, tc
cell.classList.remove('cal-day-drop');
const id = Number(e.dataTransfer.getData(DND_MIME));
if (!id) return;
const entry = entriesById.get(id);
if (!entry || entry.date === targetDate) return; // dropped on its own day
const patch = { date: targetDate, sort_order: targetCount };
if (entry.end_date) {
const delta = dayDelta(entry.date, targetDate);
patch.end_date = ymd(addDays(parseYMD(entry.end_date), delta));
}
try {
await api.entries.update(id, patch);
await tctx.refreshTrip();
} catch (err) {
toast(err.message);
}
await commitEntryMove(id, targetDate, targetCount, entriesById, tctx);
});
}
// Wire a week's stay-bands strip as a drop target, so dragging a chip/band
// onto a spot already covered by another stay's band still moves the entry
// (day cells sit below the bands strip and are otherwise unreachable there).
// The target day is derived from the pointer's x-position across the 7-column
// strip; the moved entry is appended (sort_order 0 — bands don't track a
// day's chip order).
export function makeWeekBandsDropTarget(strip, weekYmd, entriesById, tctx) {
strip.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
strip.classList.add('cal-bands-drop');
});
strip.addEventListener('dragleave', (e) => {
if (!strip.contains(e.relatedTarget)) strip.classList.remove('cal-bands-drop');
});
strip.addEventListener('drop', async (e) => {
e.preventDefault();
strip.classList.remove('cal-bands-drop');
const id = Number(e.dataTransfer.getData(DND_MIME));
if (!id) return;
const rect = strip.getBoundingClientRect();
const idx = Math.min(6, Math.max(0, Math.floor((e.clientX - rect.left) / (rect.width / 7))));
await commitEntryMove(id, weekYmd[idx], 0, entriesById, tctx);
});
}