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:
parent
63e8e96d52
commit
df24316837
5 changed files with 170 additions and 78 deletions
|
@ -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 -->
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue