1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 04:49:37 +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> </div>
{/if} {/if}
{#if lodging.check_in && lodging.check_out} <div class="space-y-3">
<div class="flex items-center gap-2"> {#if lodging.check_in}
<span class="text-sm font-medium">{$t('adventures.dates')}:</span> <div class="flex gap-2 text-sm">
<p class="text-sm"> <span class="font-medium whitespace-nowrap">{$t('adventures.check_in')}:</span>
{#if isAllDay(lodging.check_in)} <span>
{formatAllDayDate(lodging.check_in)} {#if isAllDay(lodging.check_in)}
{formatAllDayDate(lodging.check_out)} {formatAllDayDate(lodging.check_in)}
{:else} {:else}
{formatDateInTimezone(lodging.check_in, lodging.timezone)} {formatDateInTimezone(lodging.check_in, lodging.timezone)}
{formatDateInTimezone(lodging.check_out, lodging.timezone)} {#if lodging.timezone}
{#if lodging.timezone} <span class="ml-1 text-xs opacity-60">({lodging.timezone})</span>
<span class="ml-1 text-xs opacity-60">({lodging.timezone})</span> {/if}
{/if} {/if}
{/if} </span>
</p> </div>
</div> {/if}
{/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> </div>
<!-- Reservation Info --> <!-- Reservation Info -->

View file

@ -6,6 +6,7 @@
import type { Collection, Lodging } from '$lib/types'; import type { Collection, Lodging } from '$lib/types';
import LocationDropdown from './LocationDropdown.svelte'; import LocationDropdown from './LocationDropdown.svelte';
import DateRangeCollapse from './DateRangeCollapse.svelte'; import DateRangeCollapse from './DateRangeCollapse.svelte';
import { isAllDay } from '$lib';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -82,6 +83,24 @@
async function handleSubmit(event: Event) { async function handleSubmit(event: Event) {
event.preventDefault(); 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... // Create or update lodging...
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`; const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
const method = lodging.id === '' ? 'POST' : 'PATCH'; const method = lodging.id === '' ? 'POST' : 'PATCH';

View file

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

View file

@ -1,5 +1,8 @@
import inspirationalQuotes from './json/quotes.json'; import inspirationalQuotes from './json/quotes.json';
import randomBackgrounds from './json/backgrounds.json'; import randomBackgrounds from './json/backgrounds.json';
// @ts-ignore
import { DateTime } from 'luxon';
import type { import type {
Adventure, Adventure,
Background, Background,
@ -144,13 +147,6 @@ function getLocalDateString(date: Date): string {
return `${year}-${month}-${day}`; 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( export function groupTransportationsByDate(
transportations: Transportation[], transportations: Transportation[],
startDate: Date, startDate: Date,
@ -158,82 +154,127 @@ export function groupTransportationsByDate(
): Record<string, Transportation[]> { ): Record<string, Transportation[]> {
const groupedTransportations: Record<string, Transportation[]> = {}; const groupedTransportations: Record<string, Transportation[]> = {};
// Initialize all days in the range // Initialize days
for (let i = 0; i < numberOfDays; i++) { for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate); const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
currentDate.setDate(startDate.getDate() + i); const dateString = currentDate.toISODate(); // 'YYYY-MM-DD'
const dateString = getLocalDateString(currentDate);
groupedTransportations[dateString] = []; groupedTransportations[dateString] = [];
} }
transportations.forEach((transportation) => { transportations.forEach((transportation) => {
if (transportation.date) { if (transportation.date) {
const transportationDate = getLocalDateString(new Date(transportation.date)); // Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
if (transportation.end_date) { const startHasZeros = transportation.date.includes('T00:00:00');
const endDate = new Date(transportation.end_date).toISOString().split('T')[0]; 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 let startDT: DateTime;
for (let i = 0; i < numberOfDays; i++) { let endDT: DateTime;
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = getLocalDateString(currentDate);
// Include the current day if it falls within the transportation date range if (isTranspoAllDay) {
if (dateString >= transportationDate && dateString <= endDate) { // For all-day events, extract just the date part and ignore timezone
if (groupedTransportations[dateString]) { const dateOnly = transportation.date.split('T')[0]; // Get 'YYYY-MM-DD'
groupedTransportations[dateString].push(transportation); 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; return groupedTransportations;
} }
export function groupLodgingByDate( export function groupLodgingByDate(
transportations: Lodging[], lodging: Lodging[],
startDate: Date, startDate: Date,
numberOfDays: number numberOfDays: number
): Record<string, Lodging[]> { ): Record<string, Lodging[]> {
const groupedTransportations: Record<string, Lodging[]> = {}; const groupedLodging: Record<string, Lodging[]> = {};
// Initialize all days in the range using local dates // Initialize days (excluding last day for lodging)
for (let i = 0; i < numberOfDays; i++) { // If trip is 7/1 to 7/4 (4 days), show lodging only on 7/1, 7/2, 7/3
const currentDate = new Date(startDate); const lodgingDays = numberOfDays - 1;
currentDate.setDate(startDate.getDate() + i);
const dateString = getLocalDateString(currentDate); for (let i = 0; i < lodgingDays; i++) {
groupedTransportations[dateString] = []; const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
const dateString = currentDate.toISODate(); // 'YYYY-MM-DD'
groupedLodging[dateString] = [];
} }
transportations.forEach((transportation) => { lodging.forEach((hotel) => {
if (transportation.check_in) { if (hotel.check_in) {
// Use local date string conversion // Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
const transportationDate = getLocalDateString(new Date(transportation.check_in)); const startHasZeros = hotel.check_in.includes('T00:00:00');
if (transportation.check_out) { const endHasZeros = hotel.check_out ? hotel.check_out.includes('T00:00:00') : true;
const endDate = getLocalDateString(new Date(transportation.check_out)); const isAllDay = startHasZeros && endHasZeros;
// Loop through all days and include transportation if it falls within the transportation date range let startDT: DateTime;
for (let i = 0; i < numberOfDays; i++) { let endDT: DateTime;
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = getLocalDateString(currentDate);
if (dateString >= transportationDate && dateString <= endDate) { if (isAllDay) {
groupedTransportations[dateString].push(transportation); // 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( export function groupNotesByDate(
@ -292,6 +333,13 @@ export function groupChecklistsByDate(
return groupedChecklists; 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) { export function continentCodeToString(code: string) {
switch (code) { switch (code) {
case 'AF': case 'AF':

View file

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