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
+38 -5
View File
@@ -1,7 +1,8 @@
// Calendar grid for the trip's date range. Real weeks as rows (MonSun
// columns); days outside the range are greyed. Each in-range day shows its
// entries as compact, type-coloured chips. Clicking a day opens the editor.
import { el } from '../dom.js';
import { el, toast } from '../dom.js';
import { api } from '../api.js';
import {
ENTRY_TYPES,
typeInfo,
@@ -68,9 +69,14 @@ export function renderCalendar(tctx) {
{ class: 'card calendar-section' },
el(
'div',
{ class: 'section-head' },
el('h2', {}, 'Calendar'),
el('p', { class: 'muted' }, 'Click a day to add or edit entries.'),
{ class: 'section-head cal-section-head' },
el(
'div',
{},
el('h2', {}, 'Calendar'),
el('p', { class: 'muted' }, 'Click a day to add or edit entries.'),
),
syncTransportsButton(tctx),
),
legend(),
);
@@ -97,7 +103,7 @@ export function renderCalendar(tctx) {
for (const week of weeks) {
// Each week is a stay-bands strip (all-day bars) above a row of day cells.
const weekEl = el('div', { class: 'cal-week' });
const bands = renderWeekBands(week.map(ymd), stayLayout, tctx);
const bands = renderWeekBands(week.map(ymd), stayLayout, entriesById, tctx);
if (bands) weekEl.appendChild(bands);
const daysRow = el('div', { class: 'cal-week-days' });
for (const date of week) {
@@ -111,12 +117,39 @@ export function renderCalendar(tctx) {
// 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');
for (const n of cal.querySelectorAll('.cal-bands-drop')) n.classList.remove('cal-bands-drop');
});
section.appendChild(cal);
return section;
}
// "↻ Sync transports" — reconciles auto-created transport entries after stays
// have been dragged/reshuffled (see docs/API.md, "Regenerating auto-transports").
function syncTransportsButton(tctx) {
const btn = el('button', { class: 'btn btn-sm btn-ghost', type: 'button' }, '↻ Sync transports');
btn.addEventListener('click', async () => {
btn.disabled = true;
const original = btn.textContent;
btn.textContent = 'Syncing…';
try {
const res = await api.trips.regenerateTransports(tctx.tripId);
const parts = [];
if (res.created) parts.push(`${res.created} added`);
if (res.updated) parts.push(`${res.updated} updated`);
if (res.deleted) parts.push(`${res.deleted} removed`);
toast(parts.length ? `Transports synced: ${parts.join(', ')}` : 'Transports already in sync', 'success');
await tctx.refreshTrip();
} catch (err) {
toast(err.message);
} finally {
btn.disabled = false;
btn.textContent = original;
}
});
return btn;
}
function dayCell(date, rangeStart, rangeEnd, byDate, contByDate, dropoffByDate, entriesById, tctx, currency) {
const key = ymd(date);
const inRange = date >= rangeStart && date <= rangeEnd;