Refactor itinerary management and UI components

- Updated ItineraryViewSet to handle visit updates and creations more efficiently, preserving visit IDs when moving between days.
- Enhanced ChecklistCard, LodgingCard, TransportationCard, and NoteCard to include a new "Change Day" option in the actions menu.
- Improved user experience in CollectionItineraryPlanner by tracking specific itinerary items being moved and ensuring only the relevant entries are deleted.
- Added new location sharing options in LodgingCard and TransportationCard for Apple Maps, Google Maps, and OpenStreetMap.
- Updated translations in en.json for consistency and clarity.
- Minor UI adjustments for better accessibility and usability across various components.
This commit is contained in:
Sean Morley
2026-01-08 20:00:43 -05:00
parent f315f85c58
commit 1b1d801151
13 changed files with 423 additions and 157 deletions

View File

@@ -152,10 +152,12 @@ class ItineraryViewSet(viewsets.ModelViewSet):
new_start = None
new_end = None
# If we have valid bounds, update an existing Visit when provided, else create if none overlaps.
# This keeps linked data (attachments, comments) on the same Visit object.
# Update existing visit or create new one
# When moving between days, update the existing visit to preserve visit ID and data
if new_start and new_end:
source_visit_id = data.get('source_visit_id')
# If source visit provided, update it
if source_visit_id:
try:
source_visit = Visit.objects.get(id=source_visit_id, location=content_object)
@@ -163,21 +165,36 @@ class ItineraryViewSet(viewsets.ModelViewSet):
source_visit.end_date = new_end
source_visit.save(update_fields=['start_date', 'end_date'])
except Visit.DoesNotExist:
# Fall back to create-or-skip logic below
# Fall back to create logic below
pass
if not data.get('source_visit_id'):
# Overlap condition: existing.start_date <= new_end AND existing.end_date >= new_start
overlap_q = Q(start_date__lte=new_end) & Q(end_date__gte=new_start)
existing = Visit.objects.filter(location=content_object).filter(overlap_q)
if not existing.exists():
Visit.objects.create(
location=content_object,
start_date=new_start,
end_date=new_end,
notes="Created from itinerary planning"
)
# else: an overlapping visit already exists — skip creating a duplicate
# If no source visit or update failed, check for overlapping visits
if not source_visit_id:
# Check for exact match to avoid duplicates
exact_match = Visit.objects.filter(
location=content_object,
start_date=new_start,
end_date=new_end
).exists()
if not exact_match:
# Check for any overlapping visits
overlap_q = Q(start_date__lte=new_end) & Q(end_date__gte=new_start)
existing = Visit.objects.filter(location=content_object).filter(overlap_q).first()
if existing:
# Update existing overlapping visit
existing.start_date = new_start
existing.end_date = new_end
existing.save(update_fields=['start_date', 'end_date'])
else:
# Create new visit
Visit.objects.create(
location=content_object,
start_date=new_start,
end_date=new_end,
notes="Created from itinerary planning"
)
else:
# For other item types, update their date field and preserve duration
if content_type_val == 'transportation':
@@ -219,9 +236,9 @@ class ItineraryViewSet(viewsets.ModelViewSet):
# Apply same duration to new check_in
new_check_out = new_check_in + duration
else:
# No original check_out, set to same as check_in
# No original dates: check_in at midnight on selected day, check_out at midnight next day
new_check_in = datetime.datetime.combine(parse_date(clean_date), datetime.time.min)
new_check_out = new_check_in
new_check_out = new_check_in + datetime.timedelta(days=1)
content_object.check_in = new_check_in
content_object.check_out = new_check_out
@@ -334,12 +351,16 @@ class ItineraryViewSet(viewsets.ModelViewSet):
When removing a location from the itinerary, any PLANNED visits (future visits) at
that location on the same date as the itinerary item should also be removed.
If preserve_visits=true query parameter is provided, visits will NOT be deleted.
This is useful when moving items to global/trip context where we want to keep the visits.
"""
instance = self.get_object()
preserve_visits = request.query_params.get('preserve_visits', 'false').lower() == 'true'
# Check if this is a location type itinerary item
location_ct = ContentType.objects.get_for_model(Location)
if instance.content_type == location_ct and instance.object_id:
if instance.content_type == location_ct and instance.object_id and not preserve_visits:
try:
location = Location.objects.get(id=instance.object_id)
itinerary_date = instance.date
@@ -349,14 +370,11 @@ class ItineraryViewSet(viewsets.ModelViewSet):
if isinstance(itinerary_date, str):
itinerary_date = parse_date(itinerary_date)
# Find visits at this location on this date that are in the future (planned visits)
# A visit is considered "planned" if its start_date is in the future
now = timezone.now()
# Find and delete visits at this location on this date
# When removing from itinerary, we remove the associated visit
visits_to_delete = Visit.objects.filter(
location=location,
start_date__date=itinerary_date,
start_date__gt=now # Only delete future/planned visits
start_date__date=itinerary_date
)
deleted_count = visits_to_delete.count()

View File

@@ -283,20 +283,24 @@
{$t('itinerary.move_to_trip_context') || 'Move to Trip Context'}
</button>
</li>
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<Calendar class="w-4 h-4 text" />
{$t('itinerary.change_day')}
</button>
</li>
{/if}
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<Calendar class="w-4 h-4 text" />
{$t('itinerary.change_day')}
</button>
</li>
<li>
<button
on:click={() => removeFromItinerary()}
class="text-error flex items-center gap-2"
>
<CalendarRemove class="w-4 h-4 text-error" />
{$t('itinerary.remove_from_itinerary')}
{#if itineraryItem.is_global}
{$t('itinerary.remove_from_trip_context') || 'Remove from Trip Context'}
{:else}
{$t('itinerary.remove_from_itinerary')}
{/if}
</button>
</li>
{/if}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { goto } from '$app/navigation';
import type { Location, Collection, User } from '$lib/types';
const dispatch = createEventDispatcher();
@@ -40,6 +40,31 @@
let isCollectionModalOpen: boolean = false;
let isWarningModalOpen: boolean = false;
let copied: boolean = false;
let isActionsMenuOpen: boolean = false;
let actionsMenuRef: HTMLDivElement | null = null;
const ACTIONS_CLOSE_EVENT = 'location-card-close-actions';
const handleCloseEvent = () => (isActionsMenuOpen = false);
function handleDocumentClick(event: MouseEvent) {
if (!isActionsMenuOpen) return;
const target = event.target as Node | null;
if (actionsMenuRef && target && !actionsMenuRef.contains(target)) {
isActionsMenuOpen = false;
}
}
function closeAllLocationMenus() {
window.dispatchEvent(new CustomEvent(ACTIONS_CLOSE_EVENT));
}
onMount(() => {
document.addEventListener('click', handleDocumentClick);
window.addEventListener(ACTIONS_CLOSE_EVENT, handleCloseEvent);
return () => {
document.removeEventListener('click', handleDocumentClick);
window.removeEventListener(ACTIONS_CLOSE_EVENT, handleCloseEvent);
};
});
async function copyLink() {
try {
@@ -326,15 +351,39 @@
</button>
{#if !readOnly}
{#if (adventure.user && adventure.user.uuid == user?.uuid) || (collection && user && collection.shared_with?.includes(user.uuid)) || (collection && user && collection.user == user.uuid)}
<details class="dropdown dropdown-end relative z-50">
<summary class="btn btn-square btn-sm p-1 text-base-content">
<div
class="dropdown dropdown-end relative z-50"
class:dropdown-open={isActionsMenuOpen}
bind:this={actionsMenuRef}
>
<button
type="button"
class="btn btn-square btn-sm p-1 text-base-content"
aria-haspopup="menu"
aria-label={$t('adventures.location_actions') || 'Location actions'}
on:click|stopPropagation={() => {
if (isActionsMenuOpen) {
isActionsMenuOpen = false;
return;
}
closeAllLocationMenus();
isActionsMenuOpen = true;
}}
>
<DotsHorizontal class="w-5 h-5" />
</summary>
</button>
<ul
tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-[9999] w-52 p-2 shadow-lg border border-base-300"
>
<li>
<button on:click={editAdventure} class="flex items-center gap-2">
<button
on:click={() => {
isActionsMenuOpen = false;
editAdventure();
}}
class="flex items-center gap-2"
>
<FileDocumentEdit class="w-4 h-4" />
{$t('adventures.edit_location')}
</button>
@@ -342,7 +391,10 @@
{#if user?.uuid == adventure.user?.uuid}
<li>
<button
on:click={() => (isCollectionModalOpen = true)}
on:click={() => {
isActionsMenuOpen = false;
isCollectionModalOpen = true;
}}
class="flex items-center gap-2"
>
<Plus class="w-4 h-4" />
@@ -352,8 +404,10 @@
{:else if collection && user && collection.user == user.uuid}
<li>
<button
on:click={() =>
removeFromCollection(new CustomEvent('unlink', { detail: collection.id }))}
on:click={() => {
isActionsMenuOpen = false;
removeFromCollection(new CustomEvent('unlink', { detail: collection.id }));
}}
class="flex items-center gap-2"
>
<LinkVariantRemove class="w-4 h-4" />
@@ -364,7 +418,13 @@
{#if adventure.is_public}
<li>
<button on:click={copyLink} class="flex items-center gap-2">
<button
on:click={() => {
isActionsMenuOpen = false;
copyLink();
}}
class="flex items-center gap-2"
>
{#if copied}
<Check class="w-4 h-4 text-success" />
<span>{$t('adventures.link_copied')}</span>
@@ -381,30 +441,55 @@
{#if !itineraryItem.is_global}
<li>
<button
on:click={() =>
dispatch('moveToGlobal', { type: 'location', id: adventure.id })}
on:click={() => {
isActionsMenuOpen = false;
dispatch('moveToGlobal', { type: 'location', id: adventure.id });
}}
class=" flex items-center gap-2"
>
<Globe class="w-4 h-4" />
{$t('itinerary.move_to_trip_context') || 'Move to Trip Context'}
</button>
</li>
<li>
<button
on:click={() => {
isActionsMenuOpen = false;
changeDay();
}}
class=" flex items-center gap-2"
>
<Calendar class="w-4 h-4" />
{$t('itinerary.change_day')}
</button>
</li>
<li>
<button
on:click={() => {
isActionsMenuOpen = false;
removeFromItinerary();
}}
class="text-error flex items-center gap-2"
>
<CalendarRemove class="w-4 h-4 text-error" />
{$t('itinerary.remove_from_itinerary')}
</button>
</li>
{/if}
{#if itineraryItem.is_global}
<li>
<button
on:click={() => {
isActionsMenuOpen = false;
removeFromItinerary();
}}
class="text-error flex items-center gap-2"
>
<CalendarRemove class="w-4 h-4 text-error" />
{$t('itinerary.remove_from_trip_context')}
</button>
</li>
{/if}
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<Calendar class="w-4 h-4" />
{$t('itinerary.change_day')}
</button>
</li>
<li>
<button
on:click={() => removeFromItinerary()}
class="text-error flex items-center gap-2"
>
<CalendarRemove class="w-4 h-4 text-error" />
{$t('itinerary.remove_from_itinerary')}
</button>
</li>
{/if}
{#if user.uuid == adventure.user?.uuid}
@@ -414,7 +499,10 @@
id="delete_adventure"
data-umami-event="Delete Adventure"
class="text-error flex items-center gap-2"
on:click={() => (isWarningModalOpen = true)}
on:click={() => {
isActionsMenuOpen = false;
isWarningModalOpen = true;
}}
>
<TrashCan class="w-4 h-4" />
{$t('adventures.delete')}
@@ -422,7 +510,7 @@
</li>
{/if}
</ul>
</details>
</div>
{/if}
{/if}
</div>

View File

@@ -209,20 +209,24 @@
{$t('itinerary.move_to_trip_context') || 'Move to Trip Context'}
</button>
</li>
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<Calendar class="w-4 h-4 text" />
{$t('itinerary.change_day')}
</button>
</li>
{/if}
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<Calendar class="w-4 h-4 text" />
{$t('itinerary.change_day')}
</button>
</li>
<li>
<button
on:click={() => removeFromItinerary()}
class="text-error flex items-center gap-2"
>
<CalendarRemove class="w-4 h-4 text-error" />
{$t('itinerary.remove_from_itinerary')}
{#if itineraryItem.is_global}
{$t('itinerary.remove_from_trip_context') || 'Remove from Trip Context'}
{:else}
{$t('itinerary.remove_from_itinerary')}
{/if}
</button>
</li>
{/if}

View File

@@ -111,7 +111,7 @@
type="button"
class="btn btn-circle btn-ghost btn-sm"
on:click={() => (isDetailsOpen = false)}
aria-label={$t('adventures.close')}
aria-label={$t('about.close')}
>
<Close class="w-4 h-4" />
</button>
@@ -198,26 +198,30 @@
<li>
<button
on:click={() => dispatch('moveToGlobal', { type: 'note', id: note.id })}
class="text-info flex items-center gap-2"
class=" flex items-center gap-2"
>
<Globe class="w-4 h-4 text-info" />
{$t('itinerary.move_to_trip_context') || 'Move to Trip Context'}
<Globe class="w-4 h-4 " />
{$t('itinerary.move_to_trip_context')}
</button>
</li>
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<CalendarRemove class="w-4 h-4 text" />
{$t('itinerary.change_day')}
</button>
</li>
{/if}
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<CalendarRemove class="w-4 h-4 text" />
{$t('itinerary.change_day')}
</button>
</li>
<li>
<button
on:click={() => removeFromItinerary()}
class="text-error flex items-center gap-2"
>
<CalendarRemove class="w-4 h-4 text-error" />
{$t('itinerary.remove_from_itinerary')}
{#if itineraryItem.is_global}
{$t('itinerary.remove_from_trip_context') || 'Remove from Trip Context'}
{:else}
{$t('itinerary.remove_from_itinerary')}
{/if}
</button>
</li>
{/if}

View File

@@ -243,20 +243,24 @@
{$t('itinerary.move_to_trip_context') || 'Move to Trip Context'}
</button>
</li>
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<Calendar class="w-4 h-4 text" />
{$t('itinerary.change_day')}
</button>
</li>
{/if}
<li>
<button on:click={() => changeDay()} class=" flex items-center gap-2">
<Calendar class="w-4 h-4 text" />
{$t('itinerary.change_day')}
</button>
</li>
<li>
<button
on:click={() => removeFromItinerary()}
class="text-error flex items-center gap-2"
>
<CalendarRemove class="w-4 h-4 text-error" />
{$t('itinerary.remove_from_itinerary')}
{#if itineraryItem.is_global}
{$t('itinerary.remove_from_trip_context') || 'Remove from Trip Context'}
{:else}
{$t('itinerary.remove_from_itinerary')}
{/if}
</button>
</li>
{/if}

View File

@@ -174,9 +174,9 @@
.filter((it) => it.object_id === objectId && it.date && !it.is_global)
.map((it) => it.id);
// Delete the dated entries
// Delete the dated entries, but preserve visits
for (const entryId of entriesToRemove) {
await fetch(`/api/itineraries/${entryId}`, { method: 'DELETE' });
await fetch(`/api/itineraries/${entryId}?preserve_visits=true`, { method: 'DELETE' });
}
// Add as global if not already there
@@ -358,6 +358,7 @@
let dayPickItemToAdd: { type: string; item: any } | null = null;
let dayPickScheduledDates: string[] = [];
let dayPickSourceVisit: { id: string; start_date: string } | null = null;
let dayPickSourceItineraryItemId: string | null = null; // Track which specific itinerary item is being moved
// When opening a "create new item" modal we store the target date here
let pendingAddDate: string | null = null;
@@ -1064,6 +1065,16 @@
.filter((it) => it.object_id === item.id && it.date && !it.is_global)
.map((it) => it.date as string);
// If moving from a specific itinerary date, track which itinerary item it is
if (currentItineraryDate) {
const sourceItineraryItem = (collection.itinerary || []).find(
(it) => it.object_id === item.id && it.date === currentItineraryDate && !it.is_global
);
dayPickSourceItineraryItemId = sourceItineraryItem?.id || null;
} else {
dayPickSourceItineraryItemId = null;
}
if (type === 'location') {
// For locations, prefer the visit matching the current itinerary date
let matchedVisit = null;
@@ -1225,34 +1236,38 @@
}
}
// If we are moving the item (user chose to update the underlying date or an existing dated entry exists),
// remove old dated itinerary entries for this object on other dates.
// This ensures the item is moved rather than duplicated across days.
const toRemove = existingDatedItems.filter((it) => it.date !== selectedDate);
for (const oldItem of toRemove) {
// Only remove the specific itinerary entry being moved, not all occurrences
if (updateDate && dayPickSourceItineraryItemId) {
// Only delete the specific itinerary item that was being moved
try {
await fetch(`/api/itineraries/${oldItem.id}`, { method: 'DELETE' });
await fetch(`/api/itineraries/${dayPickSourceItineraryItemId}`, { method: 'DELETE' });
// Update local state to reflect removal
collection.itinerary = (collection.itinerary || []).filter(
(it) => it.id !== dayPickSourceItineraryItemId
);
days = groupItemsByDay(collection);
} catch (e) {
console.error('Failed to remove old itinerary item', oldItem.id, e);
console.error('Failed to remove source itinerary item', dayPickSourceItineraryItemId, e);
}
}
if (toRemove.length > 0) {
// Update local state to reflect removals
collection.itinerary = (collection.itinerary || []).filter(
(it) => !toRemove.some((r) => r.id === it.id)
);
days = groupItemsByDay(collection);
}
} finally {
// Reset state regardless of success/failure
dayPickItemToAdd = null;
dayPickScheduledDates = [];
dayPickSourceVisit = null;
dayPickSourceItineraryItemId = null;
isDayPickModalOpen = false;
}
}
// Helper: validate UUID format to avoid sending temporary IDs to backend
function isValidUUID(id: string | undefined | null): boolean {
if (!id) return false;
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
return uuidRegex.test(id);
}
// Add an itinerary item locally and attempt to persist to backend
async function addItineraryItemForObject(
objectType: string,
@@ -1289,10 +1304,10 @@
date: dateISO,
order,
update_item_date: updateItemDate,
// Prefer updating an existing Visit when moving a location
// Pass source visit ID so backend can update the existing visit
source_visit_id:
objectType === 'location' && updateItemDate && dayPickSourceVisit?.id
? dayPickSourceVisit.id
objectType === 'location' && updateItemDate && isValidUUID(dayPickSourceVisit?.id)
? dayPickSourceVisit!.id
: undefined
})
});
@@ -1324,42 +1339,66 @@
} else if (updateItemDate) {
// Fallback: if server didn't return updated_object, do manual update for other types
const isoDate = `${dateISO}T00:00:00`;
const nextDayISO = DateTime.fromISO(dateISO).plus({ days: 1 }).toISODate();
if (objectType === 'location') {
// For locations, update existing source visit when available; otherwise append a new visit
const sourceId = dayPickSourceVisit?.id;
// Shift the existing visit dates locally to match the new itinerary date
if (collection.locations) {
collection.locations = collection.locations.map((loc) => {
if (loc.id !== objectId) return loc;
const visits = [...(loc.visits || [])];
if (sourceId) {
const idx = visits.findIndex((v) => v.id === sourceId);
if (idx !== -1) {
visits[idx] = {
...visits[idx],
start_date: `${dateISO}T00:00:00`,
end_date: `${dateISO}T23:59:59`
};
return { ...loc, visits };
}
const visits = loc.visits || [];
// Prefer matching by visit id (source_visit) then by old date
const sourceId = dayPickSourceVisit?.id;
const oldDate = dayPickSourceVisit?.start_date
? dayPickSourceVisit.start_date.split('T')[0]
: null;
let idx = sourceId ? visits.findIndex((v) => v.id === sourceId) : -1;
if (idx === -1 && oldDate) {
idx = visits.findIndex((v) => v.start_date?.startsWith(oldDate));
}
// No source visit to update; append a new one
const newVisit = {
id: `temp-visit-${Date.now()}`,
location: objectId,
start_date: `${dateISO}T00:00:00`,
end_date: `${dateISO}T23:59:59`,
notes: $t('itinerary.visit_created_via_itinerary'),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
images: [],
attachments: []
};
return { ...loc, visits: [...(loc.visits || []), newVisit] };
if (idx === -1) return loc;
const v = visits[idx];
const startDT = v.start_date ? DateTime.fromISO(v.start_date) : null;
const endDT = v.end_date ? DateTime.fromISO(v.end_date) : null;
const baseStart = DateTime.fromISO(dateISO);
const newStart = startDT
? baseStart
.set({
second: startDT.second,
minute: startDT.minute,
hour: startDT.hour,
millisecond: startDT.millisecond
})
.toISO()
: `${dateISO}T00:00:00`;
const newEnd = endDT
? DateTime.fromISO(dateISO)
.set({
second: endDT.second,
minute: endDT.minute,
hour: endDT.hour,
millisecond: endDT.millisecond
})
.toISO()
: `${dateISO}T23:59:59`;
const nextVisits = [...visits];
nextVisits[idx] = { ...v, start_date: newStart, end_date: newEnd };
return { ...loc, visits: nextVisits };
});
}
} else if (objectType === 'lodging') {
if (collection.lodging) {
// Set check_in to selected day, check_out to next day
const checkOutDate = DateTime.fromISO(dateISO).plus({ days: 1 }).toISODate();
collection.lodging = collection.lodging.map((l) =>
l.id === objectId
? { ...l, check_in: `${dateISO}T00:00:00`, check_out: `${checkOutDate}T00:00:00` }
: l
);
}
} else if (objectType === 'note') {
if (collection.notes) {
collection.notes = collection.notes.map((n) =>
@@ -1558,6 +1597,7 @@
dayPickItemToAdd = null;
dayPickScheduledDates = [];
dayPickSourceVisit = null;
dayPickSourceItineraryItemId = null;
}}
/>
{/if}
@@ -2129,7 +2169,8 @@
handleOpenDayPickerForItem(
e.detail.type,
e.detail.item,
e.detail.forcePicker
e.detail.forcePicker,
day.date
)}
/>
{:else if objectType === 'lodging'}
@@ -2146,7 +2187,8 @@
handleOpenDayPickerForItem(
e.detail.type,
e.detail.item,
e.detail.forcePicker
e.detail.forcePicker,
day.date
)}
/>
{:else if objectType === 'note'}
@@ -2164,7 +2206,8 @@
handleOpenDayPickerForItem(
e.detail.type,
e.detail.item,
e.detail.forcePicker
e.detail.forcePicker,
day.date
)}
/>
{:else if objectType === 'checklist'}
@@ -2182,7 +2225,8 @@
handleOpenDayPickerForItem(
e.detail.type,
e.detail.item,
e.detail.forcePicker
e.detail.forcePicker,
day.date
)}
/>
{/if}

View File

@@ -113,7 +113,7 @@
disabled={isScheduled}
on:click={() => handleDaySelect(day.date, true)}
>
{isScheduled ? 'Already scheduled' : 'Update to this day'}
{isScheduled ? 'Already scheduled' : 'Move to this day'}
</button>
</div>
</div>

View File

@@ -11,7 +11,7 @@
import LodgingCard from '$lib/components/cards/LodgingCard.svelte';
import NoteCard from '$lib/components/cards/NoteCard.svelte';
import ChecklistCard from '$lib/components/cards/ChecklistCard.svelte';
import { t } from 'svelte-i18n';
import { date, t } from 'svelte-i18n';
const dispatch = createEventDispatcher();
@@ -254,6 +254,7 @@
<CalendarBlank class="w-5 h-5 text-primary" />
Already added on this day
</h4>
<p class="text-sm opacity-60 mb-4">These items are already scheduled for this day.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
{#each groupedItems.scheduledOnThisDay as { type, item, dates }}
<div class="card bg-base-100 border border-base-300 shadow-sm">
@@ -282,10 +283,8 @@
<ChecklistCard checklist={item} {user} {collection} readOnly={true} />
{/if}
</div>
<div class="text-xs opacity-70 mb-2">Already on: {(dates || []).join(', ')}</div>
<div class="text-2xs opacity-50">
Use "Items on other days" or "Add as is" to duplicate onto this date.
</div>
<div class="text-xs opacity-70 mb-2">Already on this day</div>
<button class="btn btn-xs btn-disabled w-full" disabled> Already Added </button>
</div>
</div>
{/each}
@@ -301,8 +300,8 @@
<div class="mb-4">
<h4 class="text-lg font-semibold mb-3 opacity-70">Already added on other days</h4>
<p class="text-sm opacity-60 mb-4">
These items are already in your itinerary on a different date — you can add them again
to this day or update their date.
These items are scheduled on different dates. Adding them here will update their date or
add them as-is.
</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
{#each groupedItems.scheduledOtherDays as { type, item, dates }}
@@ -334,13 +333,13 @@
</div>
<div class="text-xs opacity-70 mb-2">On: {(dates || []).join(', ')}</div>
<div class="flex gap-2">
<button
<!-- <button
class="btn btn-outline btn-xs flex-1"
on:click={() => handleAddItem(type, item.id, false)}>Add to This Day</button
>
on:click={() => handleAddItem(type, item.id, false)}>Add (Keep Date)</button
> -->
<button
class="btn btn-primary btn-xs flex-1"
on:click={() => handleAddItem(type, item.id, true)}>Add & Update Date</button
on:click={() => handleAddItem(type, item.id, true)}>Add Here</button
>
</div>
</div>
@@ -387,17 +386,17 @@
{/if}
</div>
<div class="flex gap-2">
<button
<!-- <button
class="btn btn-outline btn-xs flex-1"
on:click={() => handleAddItem(type, item.id, false)}
>
Add as is
</button>
</button> -->
<button
class="btn btn-primary btn-xs flex-1"
on:click={() => handleAddItem(type, item.id, true)}
>
Add & Update Date
Add Here
</button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
export let appVersion = 'v0.12.0-pre-dev-010726';
export let appVersion = 'v0.12.0-pre-dev-010826';
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.11.0';
export let appTitle = 'AdventureLog';
export let copyrightYear = '2023-2026';

View File

@@ -1196,7 +1196,8 @@
"item_singular": "item",
"item_plural": "items",
"already_scheduled": "Already scheduled",
"update_to_this_day": "Update to this day"
"update_to_this_day": "Update to this day",
"remove_from_trip_context": "Remove from Context"
},
"common": {
"show_less": "Hide details",

View File

@@ -417,10 +417,38 @@
</MapLibre>
</div>
{#if lodging.location}
<p class="mt-4 text-base-content/70 flex items-center gap-2">
<MapMarker class="w-5 h-5" />
{lodging.location}
</p>
<div class="rounded-lg p-3 mb-3 bg-gradient-to-br from-primary/10 to-secondary/10">
<p class="flex items-center gap-2 text-sm mb-2">
<MapMarker class="w-4 h-4" />
{lodging.location}
</p>
<div class="grid grid-cols-3 gap-2">
<a
class="btn btn-sm btn-outline hover:btn-neutral"
href={`https://maps.apple.com/?q=${encodeURIComponent(lodging.location)}`}
target="_blank"
rel="noopener noreferrer"
>
🍎 Apple
</a>
<a
class="btn btn-sm btn-outline hover:btn-accent"
href={`https://maps.google.com/?q=${encodeURIComponent(lodging.location)}`}
target="_blank"
rel="noopener noreferrer"
>
🌍 Google
</a>
<a
class="btn btn-sm btn-outline hover:btn-primary"
href={`https://www.openstreetmap.org/search?query=${encodeURIComponent(lodging.location)}`}
target="_blank"
rel="noopener noreferrer"
>
🗺️ OSM
</a>
</div>
</div>
{/if}
</div>
</div>

View File

@@ -629,6 +629,78 @@
<MapMarker class="w-5 h-5" />
{getRouteLabel()}
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3">
{#if transportation.from_location}
<div class="rounded-lg p-3 bg-gradient-to-br from-primary/10 to-secondary/10">
<p class="flex items-center gap-2 text-sm mb-2">
<MapMarker class="w-4 h-4" />
{transportation.from_location}
</p>
<div class="grid grid-cols-3 gap-2">
<a
class="btn btn-sm btn-outline hover:btn-neutral"
href={`https://maps.apple.com/?q=${encodeURIComponent(transportation.from_location)}`}
target="_blank"
rel="noopener noreferrer"
>
🍎 Apple
</a>
<a
class="btn btn-sm btn-outline hover:btn-accent"
href={`https://maps.google.com/?q=${encodeURIComponent(transportation.from_location)}`}
target="_blank"
rel="noopener noreferrer"
>
🌍 Google
</a>
<a
class="btn btn-sm btn-outline hover:btn-primary"
href={`https://www.openstreetmap.org/search?query=${encodeURIComponent(transportation.from_location)}`}
target="_blank"
rel="noopener noreferrer"
>
🗺️ OSM
</a>
</div>
</div>
{/if}
{#if transportation.to_location}
<div class="rounded-lg p-3 bg-gradient-to-br from-primary/10 to-secondary/10">
<p class="flex items-center gap-2 text-sm mb-2">
<MapMarker class="w-4 h-4" />
{transportation.to_location}
</p>
<div class="grid grid-cols-3 gap-2">
<a
class="btn btn-sm btn-outline hover:btn-neutral"
href={`https://maps.apple.com/?q=${encodeURIComponent(transportation.to_location)}`}
target="_blank"
rel="noopener noreferrer"
>
🍎 Apple
</a>
<a
class="btn btn-sm btn-outline hover:btn-accent"
href={`https://maps.google.com/?q=${encodeURIComponent(transportation.to_location)}`}
target="_blank"
rel="noopener noreferrer"
>
🌍 Google
</a>
<a
class="btn btn-sm btn-outline hover:btn-primary"
href={`https://www.openstreetmap.org/search?query=${encodeURIComponent(transportation.to_location)}`}
target="_blank"
rel="noopener noreferrer"
>
🗺️ OSM
</a>
</div>
</div>
{/if}
</div>
{/if}
</div>
</div>