mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-12-23 22:58:17 -05:00
[BUG] Single day Collections will think location visits are out of date range
Fixes #827
This commit is contained in:
@@ -9,29 +9,18 @@
|
|||||||
import TrashCan from '~icons/mdi/trash-can';
|
import TrashCan from '~icons/mdi/trash-can';
|
||||||
import Calendar from '~icons/mdi/calendar';
|
import Calendar from '~icons/mdi/calendar';
|
||||||
import DeleteWarning from './DeleteWarning.svelte';
|
import DeleteWarning from './DeleteWarning.svelte';
|
||||||
|
import { isEntityOutsideCollectionDateRange } from '$lib/dateUtils';
|
||||||
|
|
||||||
export let checklist: Checklist;
|
export let checklist: Checklist;
|
||||||
export let user: User | null = null;
|
export let user: User | null = null;
|
||||||
export let collection: Collection | null = null;
|
export let collection: Collection;
|
||||||
|
|
||||||
let isWarningModalOpen: boolean = false;
|
let isWarningModalOpen: boolean = false;
|
||||||
|
|
||||||
let unlinked: boolean = false;
|
let outsideCollectionRange: boolean = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (collection?.start_date && collection.end_date) {
|
outsideCollectionRange = isEntityOutsideCollectionDateRange(checklist, collection);
|
||||||
const startOutsideRange =
|
|
||||||
checklist.date &&
|
|
||||||
collection.start_date < checklist.date &&
|
|
||||||
collection.end_date < checklist.date;
|
|
||||||
|
|
||||||
const endOutsideRange =
|
|
||||||
checklist.date &&
|
|
||||||
collection.start_date > checklist.date &&
|
|
||||||
collection.end_date > checklist.date;
|
|
||||||
|
|
||||||
unlinked = !!(startOutsideRange || endOutsideRange || !checklist.date);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function editChecklist() {
|
function editChecklist() {
|
||||||
@@ -71,7 +60,7 @@
|
|||||||
<h2 class="text-xl font-bold break-words">{checklist.name}</h2>
|
<h2 class="text-xl font-bold break-words">{checklist.name}</h2>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="badge badge-primary">{$t('adventures.checklist')}</div>
|
<div class="badge badge-primary">{$t('adventures.checklist')}</div>
|
||||||
{#if unlinked}
|
{#if outsideCollectionRange}
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
let items: ChecklistItem[] = [];
|
let items: ChecklistItem[] = [];
|
||||||
|
|
||||||
let constrainDates: boolean = false;
|
let constrainDates: boolean = true;
|
||||||
|
|
||||||
items = checklist?.items || [];
|
items = checklist?.items || [];
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
import StarOutline from '~icons/mdi/star-outline';
|
import StarOutline from '~icons/mdi/star-outline';
|
||||||
import Eye from '~icons/mdi/eye';
|
import Eye from '~icons/mdi/eye';
|
||||||
import EyeOff from '~icons/mdi/eye-off';
|
import EyeOff from '~icons/mdi/eye-off';
|
||||||
|
import { isEntityOutsideCollectionDateRange } from '$lib/dateUtils';
|
||||||
|
|
||||||
export let type: string | null = null;
|
export let type: string | null = null;
|
||||||
export let user: User | null;
|
export let user: User | null;
|
||||||
@@ -48,17 +49,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let unlinked: boolean = false;
|
let outsideCollectionRange: boolean = false;
|
||||||
|
|
||||||
// Reactive block to update `unlinked` when dependencies change
|
|
||||||
$: {
|
$: {
|
||||||
if (collection && collection?.start_date && collection.end_date) {
|
if (collection) {
|
||||||
unlinked = adventure.visits.every((visit) => {
|
outsideCollectionRange = adventure.visits.every((visit) =>
|
||||||
if (!visit.start_date || !visit.end_date) return true;
|
isEntityOutsideCollectionDateRange(visit, collection)
|
||||||
const isBeforeVisit = collection.end_date && collection.end_date < visit.start_date;
|
);
|
||||||
const isAfterVisit = collection.start_date && collection.start_date > visit.end_date;
|
|
||||||
return isBeforeVisit || isAfterVisit;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +196,7 @@
|
|||||||
>
|
>
|
||||||
{adventure.is_visited ? $t('adventures.visited') : $t('adventures.planned')}
|
{adventure.is_visited ? $t('adventures.visited') : $t('adventures.planned')}
|
||||||
</div>
|
</div>
|
||||||
{#if unlinked}
|
{#if outsideCollectionRange}
|
||||||
<div class="badge badge-sm badge-error shadow-lg">{$t('adventures.out_of_range')}</div>
|
<div class="badge badge-sm badge-error shadow-lg">{$t('adventures.out_of_range')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import DeleteWarning from './DeleteWarning.svelte';
|
import DeleteWarning from './DeleteWarning.svelte';
|
||||||
import { LODGING_TYPES_ICONS } from '$lib';
|
import { LODGING_TYPES_ICONS } from '$lib';
|
||||||
import { formatDateInTimezone } from '$lib/dateUtils';
|
import { formatDateInTimezone, isEntityOutsideCollectionDateRange } from '$lib/dateUtils';
|
||||||
import { formatAllDayDate } from '$lib/dateUtils';
|
import { formatAllDayDate } from '$lib/dateUtils';
|
||||||
import { isAllDay } from '$lib';
|
import { isAllDay } from '$lib';
|
||||||
import CardCarousel from './CardCarousel.svelte';
|
import CardCarousel from './CardCarousel.svelte';
|
||||||
@@ -31,38 +31,11 @@
|
|||||||
dispatch('edit', lodging);
|
dispatch('edit', lodging);
|
||||||
}
|
}
|
||||||
|
|
||||||
let unlinked: boolean = false;
|
let outsideCollectionRange: boolean = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (collection?.start_date && collection.end_date) {
|
if (collection) {
|
||||||
// Parse transportation dates
|
outsideCollectionRange = isEntityOutsideCollectionDateRange(lodging, collection);
|
||||||
let transportationStartDate = lodging.check_in
|
|
||||||
? new Date(lodging.check_in.split('T')[0]) // Ensure proper date parsing
|
|
||||||
: null;
|
|
||||||
let transportationEndDate = lodging.check_out
|
|
||||||
? new Date(lodging.check_out.split('T')[0])
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Parse collection dates
|
|
||||||
let collectionStartDate = new Date(collection.start_date);
|
|
||||||
let collectionEndDate = new Date(collection.end_date);
|
|
||||||
|
|
||||||
// Check if the collection range is outside the transportation range
|
|
||||||
const startOutsideRange =
|
|
||||||
transportationStartDate &&
|
|
||||||
collectionStartDate < transportationStartDate &&
|
|
||||||
collectionEndDate < transportationStartDate;
|
|
||||||
|
|
||||||
const endOutsideRange =
|
|
||||||
transportationEndDate &&
|
|
||||||
collectionStartDate > transportationEndDate &&
|
|
||||||
collectionEndDate > transportationEndDate;
|
|
||||||
|
|
||||||
unlinked = !!(
|
|
||||||
startOutsideRange ||
|
|
||||||
endOutsideRange ||
|
|
||||||
(!transportationStartDate && !transportationEndDate)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +93,7 @@
|
|||||||
{$t(`lodging.${lodging.type}`)}
|
{$t(`lodging.${lodging.type}`)}
|
||||||
{getLodgingIcon(lodging.type)}
|
{getLodgingIcon(lodging.type)}
|
||||||
</div>
|
</div>
|
||||||
{#if unlinked}
|
{#if outsideCollectionRange}
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,23 +15,18 @@
|
|||||||
import TrashCan from '~icons/mdi/trash-can';
|
import TrashCan from '~icons/mdi/trash-can';
|
||||||
import Calendar from '~icons/mdi/calendar';
|
import Calendar from '~icons/mdi/calendar';
|
||||||
import DeleteWarning from './DeleteWarning.svelte';
|
import DeleteWarning from './DeleteWarning.svelte';
|
||||||
|
import { isEntityOutsideCollectionDateRange } from '$lib/dateUtils';
|
||||||
|
|
||||||
export let note: Note;
|
export let note: Note;
|
||||||
export let user: User | null = null;
|
export let user: User | null = null;
|
||||||
export let collection: Collection | null = null;
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
let isWarningModalOpen: boolean = false;
|
let isWarningModalOpen: boolean = false;
|
||||||
let unlinked: boolean = false;
|
let outsideCollectionRange: boolean = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (collection?.start_date && collection.end_date) {
|
if (collection) {
|
||||||
const startOutsideRange =
|
outsideCollectionRange = isEntityOutsideCollectionDateRange(note, collection);
|
||||||
note.date && collection.start_date < note.date && collection.end_date < note.date;
|
|
||||||
|
|
||||||
const endOutsideRange =
|
|
||||||
note.date && collection.start_date > note.date && collection.end_date > note.date;
|
|
||||||
|
|
||||||
unlinked = !!(startOutsideRange || endOutsideRange || !note.date);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +68,7 @@
|
|||||||
<h2 class="text-xl font-bold break-words">{note.name}</h2>
|
<h2 class="text-xl font-bold break-words">{note.name}</h2>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="badge badge-primary">{$t('adventures.note')}</div>
|
<div class="badge badge-primary">{$t('adventures.note')}</div>
|
||||||
{#if unlinked}
|
{#if outsideCollectionRange}
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
export let collection: Collection;
|
export let collection: Collection;
|
||||||
export let user: User | null = null;
|
export let user: User | null = null;
|
||||||
|
|
||||||
let constrainDates: boolean = false;
|
let constrainDates: boolean = true;
|
||||||
|
|
||||||
let isReadOnly =
|
let isReadOnly =
|
||||||
!(note && user?.uuid == note?.user) &&
|
!(note && user?.uuid == note?.user) &&
|
||||||
|
|||||||
@@ -8,7 +8,11 @@
|
|||||||
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 { formatAllDayDate, formatDateInTimezone } from '$lib/dateUtils';
|
import {
|
||||||
|
formatAllDayDate,
|
||||||
|
formatDateInTimezone,
|
||||||
|
isEntityOutsideCollectionDateRange
|
||||||
|
} from '$lib/dateUtils';
|
||||||
import { isAllDay } from '$lib';
|
import { isAllDay } from '$lib';
|
||||||
import CardCarousel from './CardCarousel.svelte';
|
import CardCarousel from './CardCarousel.svelte';
|
||||||
|
|
||||||
@@ -36,52 +40,11 @@
|
|||||||
dispatch('edit', transportation);
|
dispatch('edit', transportation);
|
||||||
}
|
}
|
||||||
|
|
||||||
let unlinked: boolean = false;
|
let outsideCollectionRange: boolean = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (collection?.start_date && collection.end_date) {
|
if (collection) {
|
||||||
// Parse transportation dates
|
outsideCollectionRange = isEntityOutsideCollectionDateRange(transportation, collection);
|
||||||
let transportationStartDate = transportation.date
|
|
||||||
? new Date(transportation.date.split('T')[0]) // Ensure proper date parsing
|
|
||||||
: null;
|
|
||||||
let transportationEndDate = transportation.end_date
|
|
||||||
? new Date(transportation.end_date.split('T')[0])
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Parse collection dates
|
|
||||||
let collectionStartDate = new Date(collection.start_date);
|
|
||||||
let collectionEndDate = new Date(collection.end_date);
|
|
||||||
|
|
||||||
// // Debugging outputs
|
|
||||||
// console.log(
|
|
||||||
// 'Transportation Start Date:',
|
|
||||||
// transportationStartDate,
|
|
||||||
// 'Transportation End Date:',
|
|
||||||
// transportationEndDate
|
|
||||||
// );
|
|
||||||
// console.log(
|
|
||||||
// 'Collection Start Date:',
|
|
||||||
// collectionStartDate,
|
|
||||||
// 'Collection End Date:',
|
|
||||||
// collectionEndDate
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Check if the collection range is outside the transportation range
|
|
||||||
const startOutsideRange =
|
|
||||||
transportationStartDate &&
|
|
||||||
collectionStartDate < transportationStartDate &&
|
|
||||||
collectionEndDate < transportationStartDate;
|
|
||||||
|
|
||||||
const endOutsideRange =
|
|
||||||
transportationEndDate &&
|
|
||||||
collectionStartDate > transportationEndDate &&
|
|
||||||
collectionEndDate > transportationEndDate;
|
|
||||||
|
|
||||||
unlinked = !!(
|
|
||||||
startOutsideRange ||
|
|
||||||
endOutsideRange ||
|
|
||||||
(!transportationStartDate && !transportationEndDate)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +128,7 @@
|
|||||||
{#if transportation.type === 'plane' && transportation.flight_number}
|
{#if transportation.type === 'plane' && transportation.flight_number}
|
||||||
<div class="badge badge-neutral">{transportation.flight_number}</div>
|
<div class="badge badge-neutral">{transportation.flight_number}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if unlinked}
|
{#if outsideCollectionRange}
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -173,6 +173,10 @@
|
|||||||
Math.round(transportation.destination_longitude * 1e6) / 1e6;
|
Math.round(transportation.destination_longitude * 1e6) / 1e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transportation.date && !transportation.end_date) {
|
||||||
|
transportation.end_date = transportation.date;
|
||||||
|
}
|
||||||
|
|
||||||
if (!transportation.type) {
|
if (!transportation.type) {
|
||||||
transportation.type = 'other';
|
transportation.type = 'other';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import type { Checklist, Collection, Lodging, Note, Transportation, Visit } from './types';
|
||||||
|
import { isAllDay } from '$lib';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a UTC ISO date to a datetime-local value in the specified timezone
|
* Convert a UTC ISO date to a datetime-local value in the specified timezone
|
||||||
* @param utcDate - UTC date in ISO format or null
|
|
||||||
* @param timezone - Target timezone (defaults to browser timezone)
|
|
||||||
* @returns Formatted local datetime string for input fields (YYYY-MM-DDTHH:MM)
|
|
||||||
*/
|
*/
|
||||||
export function toLocalDatetime(
|
export function toLocalDatetime(
|
||||||
utcDate: string | null,
|
utcDate: string | null,
|
||||||
timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
|
timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
): string {
|
): string {
|
||||||
if (!utcDate) return '';
|
if (!utcDate) return '';
|
||||||
|
|
||||||
const dt = DateTime.fromISO(utcDate, { zone: 'UTC' });
|
const dt = DateTime.fromISO(utcDate, { zone: 'UTC' });
|
||||||
if (!dt.isValid) return '';
|
if (!dt.isValid) return '';
|
||||||
|
|
||||||
const isoString = dt.setZone(timezone).toISO({
|
const isoString = dt.setZone(timezone).toISO({
|
||||||
suppressSeconds: true,
|
suppressSeconds: true,
|
||||||
includeOffset: false
|
includeOffset: false
|
||||||
});
|
});
|
||||||
|
|
||||||
return isoString ? isoString.slice(0, 16) : '';
|
return isoString ? isoString.slice(0, 16) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a local datetime to UTC
|
* Convert a local datetime to UTC
|
||||||
* @param localDate - Local datetime string in ISO format
|
|
||||||
* @param timezone - Source timezone (defaults to browser timezone)
|
|
||||||
* @returns UTC datetime in ISO format or null
|
|
||||||
*/
|
*/
|
||||||
export function toUTCDatetime(
|
export function toUTCDatetime(
|
||||||
localDate: string,
|
localDate: string,
|
||||||
@@ -36,15 +29,12 @@ export function toUTCDatetime(
|
|||||||
allDay: boolean = false
|
allDay: boolean = false
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!localDate) return null;
|
if (!localDate) return null;
|
||||||
|
|
||||||
if (allDay) {
|
if (allDay) {
|
||||||
// Treat input as date-only, set UTC midnight manually
|
// Treat as date only, set UTC midnight
|
||||||
return DateTime.fromISO(localDate, { zone: 'UTC' })
|
return DateTime.fromISO(localDate, { zone: 'UTC' })
|
||||||
.startOf('day')
|
.startOf('day')
|
||||||
.toISO({ suppressMilliseconds: true });
|
.toISO({ suppressMilliseconds: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal timezone conversion for datetime-local input
|
|
||||||
return DateTime.fromISO(localDate, { zone: timezone })
|
return DateTime.fromISO(localDate, { zone: timezone })
|
||||||
.toUTC()
|
.toUTC()
|
||||||
.toISO({ suppressMilliseconds: true });
|
.toISO({ suppressMilliseconds: true });
|
||||||
@@ -52,8 +42,6 @@ export function toUTCDatetime(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates local datetime values based on UTC date and timezone
|
* Updates local datetime values based on UTC date and timezone
|
||||||
* @param params Object containing UTC date and timezone
|
|
||||||
* @returns Object with updated local datetime string
|
|
||||||
*/
|
*/
|
||||||
export function updateLocalDate({
|
export function updateLocalDate({
|
||||||
utcDate,
|
utcDate,
|
||||||
@@ -62,15 +50,11 @@ export function updateLocalDate({
|
|||||||
utcDate: string | null;
|
utcDate: string | null;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return { localDate: toLocalDatetime(utcDate, timezone) };
|
||||||
localDate: toLocalDatetime(utcDate, timezone)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates UTC datetime values based on local datetime and timezone
|
* Updates UTC datetime values based on local datetime and timezone
|
||||||
* @param params Object containing local date and timezone
|
|
||||||
* @returns Object with updated UTC datetime string
|
|
||||||
*/
|
*/
|
||||||
export function updateUTCDate({
|
export function updateUTCDate({
|
||||||
localDate,
|
localDate,
|
||||||
@@ -81,40 +65,27 @@ export function updateUTCDate({
|
|||||||
timezone: string;
|
timezone: string;
|
||||||
allDay?: boolean;
|
allDay?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return { utcDate: toUTCDatetime(localDate, timezone, allDay) };
|
||||||
utcDate: toUTCDatetime(localDate, timezone, allDay)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate date ranges using UTC comparison
|
* Validate date ranges using UTC comparison
|
||||||
* @param startDate - Start date string in UTC (ISO format)
|
|
||||||
* @param endDate - End date string in UTC (ISO format)
|
|
||||||
* @returns Object with validation result and optional error message
|
|
||||||
*/
|
*/
|
||||||
export function validateDateRange(
|
export function validateDateRange(
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string
|
endDate: string
|
||||||
): { valid: boolean; error?: string } {
|
): { valid: boolean; error?: string } {
|
||||||
if (endDate && !startDate) {
|
if (endDate && !startDate) {
|
||||||
return {
|
return { valid: false, error: 'Start date is required when end date is provided' };
|
||||||
valid: false,
|
|
||||||
error: 'Start date is required when end date is provided'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
startDate &&
|
startDate &&
|
||||||
endDate &&
|
endDate &&
|
||||||
DateTime.fromISO(startDate, { zone: 'utc' }).toMillis() >
|
DateTime.fromISO(startDate, { zone: 'utc' }).toMillis() >
|
||||||
DateTime.fromISO(endDate, { zone: 'utc' }).toMillis()
|
DateTime.fromISO(endDate, { zone: 'utc' }).toMillis()
|
||||||
) {
|
) {
|
||||||
return {
|
return { valid: false, error: 'Start date must be before end date (based on UTC)' };
|
||||||
valid: false,
|
|
||||||
error: 'Start date must be before end date (based on UTC)'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +106,6 @@ export function formatDateInTimezone(utcDate: string, timezone: string | null):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format UTC date for display
|
|
||||||
* @param utcDate - UTC date in ISO format
|
|
||||||
* @returns Formatted date string without seconds (YYYY-MM-DD HH:MM)
|
|
||||||
*/
|
|
||||||
export function formatUTCDate(utcDate: string | null): string {
|
export function formatUTCDate(utcDate: string | null): string {
|
||||||
if (!utcDate) return '';
|
if (!utcDate) return '';
|
||||||
const dateTime = DateTime.fromISO(utcDate);
|
const dateTime = DateTime.fromISO(utcDate);
|
||||||
@@ -147,18 +113,10 @@ export function formatUTCDate(utcDate: string | null): string {
|
|||||||
return dateTime.toISO()?.slice(0, 16).replace('T', ' ') || '';
|
return dateTime.toISO()?.slice(0, 16).replace('T', ' ') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format all-day date for display without timezone conversion
|
|
||||||
* @param dateString - Date string in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
|
|
||||||
* @returns Formatted date string (e.g., "Jun 1, 2025")
|
|
||||||
*/
|
|
||||||
export function formatAllDayDate(dateString: string): string {
|
export function formatAllDayDate(dateString: string): string {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
|
|
||||||
// Extract just the date part and add midday time to avoid timezone issues
|
|
||||||
const datePart = dateString.split('T')[0];
|
const datePart = dateString.split('T')[0];
|
||||||
const dateWithMidday = `${datePart}T12:00:00`;
|
const dateWithMidday = `${datePart}T12:00:00`;
|
||||||
|
|
||||||
return new Intl.DateTimeFormat('en-US', {
|
return new Intl.DateTimeFormat('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -166,6 +124,150 @@ export function formatAllDayDate(dateString: string): string {
|
|||||||
}).format(new Date(dateWithMidday));
|
}).format(new Date(dateWithMidday));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==== FIXED TIMEZONE-AWARE DATE RANGE LOGIC ====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts start and end dates from various entity types (Luxon DateTime)
|
||||||
|
* Returns also isAllDay flag for correct comparison logic
|
||||||
|
*/
|
||||||
|
function getEntityDateRange(entity: Visit | Transportation | Lodging | Note | Checklist): {
|
||||||
|
start: DateTime | null;
|
||||||
|
end: DateTime | null;
|
||||||
|
isAllDay: boolean;
|
||||||
|
} {
|
||||||
|
let start: DateTime | null = null;
|
||||||
|
let end: DateTime | null = null;
|
||||||
|
let isAllDayEvent = false;
|
||||||
|
try {
|
||||||
|
let timezone = (entity as Visit).timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
if ('start_date' in entity && 'end_date' in entity) {
|
||||||
|
// Check if all-day (no time portion)
|
||||||
|
isAllDayEvent = isAllDay(entity.start_date) && isAllDay(entity.end_date);
|
||||||
|
console;
|
||||||
|
if (isAllDayEvent) {
|
||||||
|
start = entity.start_date
|
||||||
|
? DateTime.fromISO(entity.start_date.split('T')[0], { zone: 'UTC' }).startOf('day')
|
||||||
|
: null;
|
||||||
|
end = entity.end_date
|
||||||
|
? DateTime.fromISO(entity.end_date.split('T')[0], { zone: 'UTC' }).endOf('day')
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
start = DateTime.fromISO(entity.start_date, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
end = DateTime.fromISO(entity.end_date, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
}
|
||||||
|
} else if ('date' in entity && 'end_date' in entity) {
|
||||||
|
isAllDayEvent = !!(entity.date && entity.date.length === 10);
|
||||||
|
if (isAllDayEvent) {
|
||||||
|
start = DateTime.fromISO(entity.date, { zone: 'UTC' }).startOf('day');
|
||||||
|
end = DateTime.fromISO(entity.end_date, { zone: 'UTC' }).endOf('day');
|
||||||
|
} else {
|
||||||
|
start = DateTime.fromISO(entity.date, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
end = DateTime.fromISO(entity.end_date, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
}
|
||||||
|
} else if ('check_in' in entity && 'check_out' in entity) {
|
||||||
|
isAllDayEvent = !!(entity.check_in && entity.check_in.length === 10);
|
||||||
|
if (isAllDayEvent) {
|
||||||
|
start = DateTime.fromISO(entity.check_in, { zone: 'UTC' }).startOf('day');
|
||||||
|
end = DateTime.fromISO(entity.check_out, { zone: 'UTC' }).endOf('day');
|
||||||
|
} else {
|
||||||
|
start = DateTime.fromISO(entity.check_in, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
end = DateTime.fromISO(entity.check_out, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
}
|
||||||
|
} else if ('date' in entity) {
|
||||||
|
isAllDayEvent = !!(entity.date && entity.date.length === 10);
|
||||||
|
if (isAllDayEvent) {
|
||||||
|
start = DateTime.fromISO(entity.date, { zone: 'UTC' }).startOf('day');
|
||||||
|
end = start;
|
||||||
|
} else {
|
||||||
|
start = DateTime.fromISO(entity.date, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
end = start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing entity dates:', error);
|
||||||
|
}
|
||||||
|
return { start, end, isAllDay: isAllDayEvent };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract collection start/end as Luxon DateTime w/allDay logic
|
||||||
|
*/
|
||||||
|
function getCollectionDateRange(collection: Collection): {
|
||||||
|
start: DateTime | null;
|
||||||
|
end: DateTime | null;
|
||||||
|
isAllDay: boolean;
|
||||||
|
} {
|
||||||
|
if (!collection.start_date || !collection.end_date) {
|
||||||
|
return { start: null, end: null, isAllDay: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume collection always uses full datetimes in ISO string
|
||||||
|
const isAllDay = collection.start_date.length === 10 && collection.end_date.length === 10;
|
||||||
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const start = isAllDay
|
||||||
|
? DateTime.fromISO(collection.start_date, { zone: 'UTC' }).startOf('day')
|
||||||
|
: DateTime.fromISO(collection.start_date, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
const end = isAllDay
|
||||||
|
? DateTime.fromISO(collection.end_date, { zone: 'UTC' }).endOf('day')
|
||||||
|
: DateTime.fromISO(collection.end_date, { zone: 'UTC' }).setZone(timezone);
|
||||||
|
return { start, end, isAllDay };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an entity falls within a collection's date range (timezone-safe, all-day-aware)
|
||||||
|
*/
|
||||||
|
export function isEntityInCollectionDateRange(
|
||||||
|
entity: Visit | Transportation | Lodging | Note | Checklist,
|
||||||
|
collection: Collection
|
||||||
|
): boolean {
|
||||||
|
if (!collection?.start_date || !collection.end_date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { start: entityStart, end: entityEnd, isAllDay: entityAllDay } = getEntityDateRange(entity);
|
||||||
|
const {
|
||||||
|
start: collStart,
|
||||||
|
end: collEnd,
|
||||||
|
isAllDay: collAllDay
|
||||||
|
} = getCollectionDateRange(collection);
|
||||||
|
|
||||||
|
// If any dates are missing, don't match
|
||||||
|
if (!entityStart || !collStart) return false;
|
||||||
|
|
||||||
|
// If either side is all-day, use date comparison
|
||||||
|
if (entityAllDay || collAllDay) {
|
||||||
|
// Compare only date components
|
||||||
|
const entStartDate = entityStart.startOf('day');
|
||||||
|
const entEndDate = (entityEnd || entityStart).endOf('day');
|
||||||
|
const colStartDate = collStart.startOf('day');
|
||||||
|
const colEndDate = collEnd.endOf('day');
|
||||||
|
return entStartDate <= colEndDate && entEndDate >= colStartDate;
|
||||||
|
} else {
|
||||||
|
// Compare actual DateTimes
|
||||||
|
const entEnd = entityEnd || entityStart;
|
||||||
|
return entityStart <= collEnd && entEnd >= collStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEntityOutsideCollectionDateRange(
|
||||||
|
entity: Visit | Transportation | Lodging | Note | Checklist,
|
||||||
|
collection: Collection
|
||||||
|
): boolean {
|
||||||
|
return !isEntityInCollectionDateRange(entity, collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEntitiesInDateRange<
|
||||||
|
T extends Visit | Transportation | Lodging | Note | Checklist
|
||||||
|
>(entities: T[], collection: Collection): T[] {
|
||||||
|
return entities.filter((entity) => isEntityInCollectionDateRange(entity, collection));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEntitiesOutsideDateRange<
|
||||||
|
T extends Visit | Transportation | Lodging | Note | Checklist
|
||||||
|
>(entities: T[], collection: Collection): T[] {
|
||||||
|
return entities.filter((entity) => isEntityOutsideCollectionDateRange(entity, collection));
|
||||||
|
}
|
||||||
|
|
||||||
export const VALID_TIMEZONES = [
|
export const VALID_TIMEZONES = [
|
||||||
'Africa/Abidjan',
|
'Africa/Abidjan',
|
||||||
'Africa/Accra',
|
'Africa/Accra',
|
||||||
|
|||||||
Reference in New Issue
Block a user