1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-18 20:39:36 +02:00

feat(lodging): improve lodging date handling with all-day event support and timezone adjustments

This commit is contained in:
Sean Morley 2025-06-18 21:10:10 -04:00
parent 63e8e96d52
commit df24316837
5 changed files with 170 additions and 78 deletions

View file

@ -120,23 +120,39 @@
</div>
{/if}
{#if lodging.check_in && lodging.check_out}
<div class="flex items-center gap-2">
<span class="text-sm font-medium">{$t('adventures.dates')}:</span>
<p class="text-sm">
{#if isAllDay(lodging.check_in)}
{formatAllDayDate(lodging.check_in)}
{formatAllDayDate(lodging.check_out)}
{:else}
{formatDateInTimezone(lodging.check_in, lodging.timezone)}
{formatDateInTimezone(lodging.check_out, lodging.timezone)}
{#if lodging.timezone}
<span class="ml-1 text-xs opacity-60">({lodging.timezone})</span>
<div class="space-y-3">
{#if lodging.check_in}
<div class="flex gap-2 text-sm">
<span class="font-medium whitespace-nowrap">{$t('adventures.check_in')}:</span>
<span>
{#if isAllDay(lodging.check_in)}
{formatAllDayDate(lodging.check_in)}
{:else}
{formatDateInTimezone(lodging.check_in, lodging.timezone)}
{#if lodging.timezone}
<span class="ml-1 text-xs opacity-60">({lodging.timezone})</span>
{/if}
{/if}
{/if}
</p>
</div>
{/if}
</span>
</div>
{/if}
{#if lodging.check_out}
<div class="flex gap-2 text-sm">
<span class="font-medium whitespace-nowrap">{$t('adventures.check_out')}:</span>
<span>
{#if isAllDay(lodging.check_out)}
{formatAllDayDate(lodging.check_out)}
{:else}
{formatDateInTimezone(lodging.check_out, lodging.timezone)}
{#if lodging.timezone}
<span class="ml-1 text-xs opacity-60">({lodging.timezone})</span>
{/if}
{/if}
</span>
</div>
{/if}
</div>
</div>
<!-- Reservation Info -->

View file

@ -6,6 +6,7 @@
import type { Collection, Lodging } from '$lib/types';
import LocationDropdown from './LocationDropdown.svelte';
import DateRangeCollapse from './DateRangeCollapse.svelte';
import { isAllDay } from '$lib';
const dispatch = createEventDispatcher();
@ -82,6 +83,24 @@
async function handleSubmit(event: Event) {
event.preventDefault();
lodging.timezone = lodgingTimezone || null;
// Auto-set end date if missing but start date exists
if (lodging.check_in && !lodging.check_out) {
const startDate = new Date(lodging.check_in);
const nextDay = new Date(startDate);
nextDay.setDate(nextDay.getDate() + 1);
if (isAllDay(lodging.check_in)) {
// For all-day, set to next day at 00:00:00
lodging.check_out = nextDay.toISOString().split('T')[0] + 'T00:00:00';
} else {
// For timed events, set to next day at 9:00 AM
nextDay.setHours(9, 0, 0, 0);
lodging.check_out = nextDay.toISOString();
}
}
// Create or update lodging...
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
const method = lodging.id === '' ? 'POST' : 'PATCH';

View file

@ -8,7 +8,8 @@
import DeleteWarning from './DeleteWarning.svelte';
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
import { TRANSPORTATION_TYPES_ICONS } from '$lib';
import { formatDateInTimezone } from '$lib/dateUtils';
import { formatAllDayDate, formatDateInTimezone } from '$lib/dateUtils';
import { isAllDay } from '$lib';
function getTransportationIcon(type: string) {
if (type in TRANSPORTATION_TYPES_ICONS) {
@ -161,9 +162,13 @@
<div class="flex gap-2 text-sm">
<span class="font-medium whitespace-nowrap">{$t('adventures.start')}:</span>
<span>
{formatDateInTimezone(transportation.date, transportation.start_timezone)}
{#if transportation.start_timezone}
<span class="ml-1 text-xs opacity-60">({transportation.start_timezone})</span>
{#if isAllDay(transportation.date) && (!transportation.end_date || isAllDay(transportation.end_date))}
{formatAllDayDate(transportation.date)}
{:else}
{formatDateInTimezone(transportation.date, transportation.start_timezone)}
{#if transportation.start_timezone}
<span class="ml-1 text-xs opacity-60">({transportation.start_timezone})</span>
{/if}
{/if}
</span>
</div>
@ -173,9 +178,13 @@
<div class="flex gap-2 text-sm">
<span class="font-medium whitespace-nowrap">{$t('adventures.end')}:</span>
<span>
{formatDateInTimezone(transportation.end_date, transportation.end_timezone)}
{#if transportation.end_timezone}
<span class="ml-1 text-xs opacity-60">({transportation.end_timezone})</span>
{#if isAllDay(transportation.end_date) && (!transportation.date || isAllDay(transportation.date))}
{formatAllDayDate(transportation.end_date)}
{:else}
{formatDateInTimezone(transportation.end_date, transportation.end_timezone)}
{#if transportation.end_timezone}
<span class="ml-1 text-xs opacity-60">({transportation.end_timezone})</span>
{/if}
{/if}
</span>
</div>

View file

@ -1,5 +1,8 @@
import inspirationalQuotes from './json/quotes.json';
import randomBackgrounds from './json/backgrounds.json';
// @ts-ignore
import { DateTime } from 'luxon';
import type {
Adventure,
Background,
@ -144,13 +147,6 @@ function getLocalDateString(date: Date): string {
return `${year}-${month}-${day}`;
}
// Helper to check if a given date string represents midnight (all-day)
// Improved isAllDay function to handle different ISO date formats
export function isAllDay(dateStr: string): boolean {
// Check for various midnight formats in UTC
return dateStr.endsWith('T00:00:00Z') || dateStr.endsWith('T00:00:00.000Z');
}
export function groupTransportationsByDate(
transportations: Transportation[],
startDate: Date,
@ -158,82 +154,127 @@ export function groupTransportationsByDate(
): Record<string, Transportation[]> {
const groupedTransportations: Record<string, Transportation[]> = {};
// Initialize all days in the range
// Initialize days
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = getLocalDateString(currentDate);
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
const dateString = currentDate.toISODate(); // 'YYYY-MM-DD'
groupedTransportations[dateString] = [];
}
transportations.forEach((transportation) => {
if (transportation.date) {
const transportationDate = getLocalDateString(new Date(transportation.date));
if (transportation.end_date) {
const endDate = new Date(transportation.end_date).toISOString().split('T')[0];
// Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
const startHasZeros = transportation.date.includes('T00:00:00');
const endHasZeros = transportation.end_date
? transportation.end_date.includes('T00:00:00')
: true;
const isTranspoAllDay = startHasZeros && endHasZeros;
// Loop through all days and include transportation if it falls within the range
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = getLocalDateString(currentDate);
let startDT: DateTime;
let endDT: DateTime;
// Include the current day if it falls within the transportation date range
if (dateString >= transportationDate && dateString <= endDate) {
if (groupedTransportations[dateString]) {
groupedTransportations[dateString].push(transportation);
}
}
if (isTranspoAllDay) {
// For all-day events, extract just the date part and ignore timezone
const dateOnly = transportation.date.split('T')[0]; // Get 'YYYY-MM-DD'
startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone
endDT = transportation.end_date
? DateTime.fromISO(transportation.end_date.split('T')[0])
: startDT;
} else {
// For timed events, use timezone conversion
startDT = DateTime.fromISO(transportation.date, {
zone: transportation.start_timezone ?? 'UTC'
});
endDT = transportation.end_date
? DateTime.fromISO(transportation.end_date, {
zone: transportation.end_timezone ?? transportation.start_timezone ?? 'UTC'
})
: startDT;
}
const startDateStr = startDT.toISODate();
const endDateStr = endDT.toISODate();
// Loop through all days in range
for (let i = 0; i < numberOfDays; i++) {
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
const currentDateStr = currentDate.toISODate();
if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) {
groupedTransportations[currentDateStr]?.push(transportation);
}
} else if (groupedTransportations[transportationDate]) {
// If there's no end date, add transportation to the start date only
groupedTransportations[transportationDate].push(transportation);
}
}
});
return groupedTransportations;
}
export function groupLodgingByDate(
transportations: Lodging[],
lodging: Lodging[],
startDate: Date,
numberOfDays: number
): Record<string, Lodging[]> {
const groupedTransportations: Record<string, Lodging[]> = {};
const groupedLodging: Record<string, Lodging[]> = {};
// Initialize all days in the range using local dates
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = getLocalDateString(currentDate);
groupedTransportations[dateString] = [];
// Initialize days (excluding last day for lodging)
// If trip is 7/1 to 7/4 (4 days), show lodging only on 7/1, 7/2, 7/3
const lodgingDays = numberOfDays - 1;
for (let i = 0; i < lodgingDays; i++) {
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
const dateString = currentDate.toISODate(); // 'YYYY-MM-DD'
groupedLodging[dateString] = [];
}
transportations.forEach((transportation) => {
if (transportation.check_in) {
// Use local date string conversion
const transportationDate = getLocalDateString(new Date(transportation.check_in));
if (transportation.check_out) {
const endDate = getLocalDateString(new Date(transportation.check_out));
lodging.forEach((hotel) => {
if (hotel.check_in) {
// Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
const startHasZeros = hotel.check_in.includes('T00:00:00');
const endHasZeros = hotel.check_out ? hotel.check_out.includes('T00:00:00') : true;
const isAllDay = startHasZeros && endHasZeros;
// Loop through all days and include transportation if it falls within the transportation date range
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = getLocalDateString(currentDate);
let startDT: DateTime;
let endDT: DateTime;
if (dateString >= transportationDate && dateString <= endDate) {
groupedTransportations[dateString].push(transportation);
}
if (isAllDay) {
// For all-day events, extract just the date part and ignore timezone
const dateOnly = hotel.check_in.split('T')[0]; // Get 'YYYY-MM-DD'
startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone
endDT = hotel.check_out ? DateTime.fromISO(hotel.check_out.split('T')[0]) : startDT;
} else {
// For timed events, use timezone conversion
startDT = DateTime.fromISO(hotel.check_in, {
zone: hotel.timezone ?? 'UTC'
});
endDT = hotel.check_out
? DateTime.fromISO(hotel.check_out, {
zone: hotel.timezone ?? 'UTC'
})
: startDT;
}
const startDateStr = startDT.toISODate();
const endDateStr = endDT.toISODate();
// Loop through lodging days only (excluding last day)
for (let i = 0; i < lodgingDays; i++) {
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
const currentDateStr = currentDate.toISODate();
// Show lodging on days where check-in occurs through the day before check-out
// For lodging, we typically want to show it on the nights you're staying
if (currentDateStr >= startDateStr && currentDateStr < endDateStr) {
groupedLodging[currentDateStr]?.push(hotel);
}
} else if (groupedTransportations[transportationDate]) {
groupedTransportations[transportationDate].push(transportation);
}
}
});
return groupedTransportations;
return groupedLodging;
}
export function groupNotesByDate(
@ -292,6 +333,13 @@ export function groupChecklistsByDate(
return groupedChecklists;
}
// Helper to check if a given date string represents midnight (all-day)
// Improved isAllDay function to handle different ISO date formats
export function isAllDay(dateStr: string): boolean {
// Check for various midnight formats in UTC
return dateStr.endsWith('T00:00:00Z') || dateStr.endsWith('T00:00:00.000Z');
}
export function continentCodeToString(code: string) {
switch (code) {
case 'AF':

View file

@ -1042,7 +1042,7 @@
numberOfDays + 1
)[dateString] || []}
{@const dayLodging =
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays)[
groupLodgingByDate(lodging, new Date(collection.start_date), numberOfDays + 1)[
dateString
] || []}
{@const dayNotes =