mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(calendar): give stay check-in/check-out markers the one-hour handover window (#2136)
This commit is contained in:
@@ -453,7 +453,10 @@ export class CalendarService {
|
||||
car: { start: 'Pickup', end: 'Drop-off' },
|
||||
parking: { start: 'Drop-off', end: 'Pickup' },
|
||||
};
|
||||
/** How long a hand-over reads as. Zero-length events collapse in most clients. */
|
||||
/**
|
||||
* How long a hand-over — and a check-in/check-out marker without an
|
||||
* until-clock — reads as. Zero-length events collapse in most clients.
|
||||
*/
|
||||
const HANDOVER_MINUTES = 60;
|
||||
|
||||
const plusMinutes = (date: string, time: string, minutes: number): { date: string; time: string } => {
|
||||
@@ -614,7 +617,18 @@ export class CalendarService {
|
||||
const ref = `${date}T00:00`;
|
||||
let ev = `BEGIN:VEVENT\r\nUID:${uid(stay.id, kind)}\r\nDTSTAMP:${now}\r\n`;
|
||||
ev += dtLine('DTSTART', time, zone, ref);
|
||||
if (isTime(endTime)) ev += dtLine('DTEND', endTime!, zone, ref);
|
||||
if (isTime(endTime)) {
|
||||
ev += dtLine('DTEND', endTime!, zone, ref);
|
||||
} else {
|
||||
// No recorded end → the same default hour the hand-overs read as.
|
||||
// Without a DTEND the marker is a zero-length point, and since a fully
|
||||
// timed stay is represented by nothing else, several clients rendered
|
||||
// the whole booking as clutter or hid it outright (#2136). plusMinutes
|
||||
// carries the date across midnight, so a late check-out ends on the
|
||||
// following day instead of before it started.
|
||||
const stop = plusMinutes(date, time, HANDOVER_MINUTES);
|
||||
ev += dtLine('DTEND', stop.time, zone, `${stop.date}T00:00`);
|
||||
}
|
||||
ev += `SUMMARY:${esc(summary)}\r\n`;
|
||||
if (stay.place_address) ev += `LOCATION:${esc(stay.place_address)}\r\n`;
|
||||
ev += `END:VEVENT\r\n`;
|
||||
|
||||
@@ -861,6 +861,10 @@ describe('accommodations', () => {
|
||||
expect(ics).not.toContain('DTSTART;VALUE=DATE:20260707\r\nDTEND;VALUE=DATE:20260713');
|
||||
expect(ics).toContain('SUMMARY:Check-in: Hotel Bellevue');
|
||||
expect(ics).toContain('SUMMARY:Check-out: Hotel Bellevue');
|
||||
// With the block gone the markers are the booking's whole representation, so
|
||||
// they carry the hand-over default hour instead of being points.
|
||||
expect(ics).toContain('DTEND;TZID=Europe/Paris:20260707T160000');
|
||||
expect(ics).toContain('DTEND;TZID=Europe/Paris:20260712T120000');
|
||||
});
|
||||
|
||||
it('CAL-025d: a second room on the same stay keeps its block, since no marker names it', () => {
|
||||
@@ -915,6 +919,44 @@ describe('accommodations', () => {
|
||||
expect(ics).toContain('BEGIN:VTIMEZONE\r\nTZID:Europe/Paris');
|
||||
});
|
||||
|
||||
it('CAL-026b: a check-in without an until-clock reads as one hour, not a point (#2136)', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Paris' });
|
||||
createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00' });
|
||||
|
||||
const { ics } = svc.exportICS(trip.id);
|
||||
|
||||
// Reporter variant 2: the lone timed end becomes a one-hour slot AND the
|
||||
// all-day block stays, since it is the only carrier of the other end's date.
|
||||
expect(ics).toContain('DTSTART;TZID=Europe/Paris:20260707T150000');
|
||||
expect(ics).toContain('DTEND;TZID=Europe/Paris:20260707T160000');
|
||||
expect(ics).toContain('DTSTART;VALUE=DATE:20260707\r\nDTEND;VALUE=DATE:20260713');
|
||||
});
|
||||
|
||||
it('CAL-026c: the check-out marker carries the same default hour', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Paris' });
|
||||
createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_out: '11:00' });
|
||||
|
||||
const { ics } = svc.exportICS(trip.id);
|
||||
|
||||
expect(ics).toContain('DTSTART;TZID=Europe/Paris:20260712T110000');
|
||||
expect(ics).toContain('DTEND;TZID=Europe/Paris:20260712T120000');
|
||||
});
|
||||
|
||||
it('CAL-026d: a late check-out rolls the DTEND over midnight', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Paris' });
|
||||
createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_out: '23:30' });
|
||||
|
||||
const { ics } = svc.exportICS(trip.id);
|
||||
|
||||
// Without the day shift the DTEND would land before its DTSTART, and clients
|
||||
// drop an event whose end precedes its start.
|
||||
expect(ics).toContain('DTSTART;TZID=Europe/Paris:20260712T233000');
|
||||
expect(ics).toContain('DTEND;TZID=Europe/Paris:20260713T003000');
|
||||
});
|
||||
|
||||
it('CAL-027: a stay without times emits the all-day range and nothing else', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Paris' });
|
||||
|
||||
Reference in New Issue
Block a user