diff --git a/backend/server/adventures/utils/itinerary.py b/backend/server/adventures/utils/itinerary.py
index deaf0bd0..4769ded3 100644
--- a/backend/server/adventures/utils/itinerary.py
+++ b/backend/server/adventures/utils/itinerary.py
@@ -1,5 +1,6 @@
from typing import List
from django.db import transaction
+from django.utils.dateparse import parse_date, parse_datetime
from rest_framework.exceptions import ValidationError, PermissionDenied
from adventures.models import CollectionItineraryItem
@@ -75,6 +76,27 @@ def reorder_itinerary_items(user, items_data: List[dict]):
new_date = item_data.get('date')
new_order = item_data.get('order')
if new_date is not None:
+ # validate date is within collection bounds (if collection has start/end)
+ parsed = None
+ try:
+ parsed = parse_date(str(new_date))
+ except Exception:
+ parsed = None
+ if parsed is None:
+ try:
+ dt = parse_datetime(str(new_date))
+ if dt:
+ parsed = dt.date()
+ except Exception:
+ parsed = None
+
+ collection = item.collection
+ if parsed and collection:
+ if collection.start_date and parsed < collection.start_date:
+ raise ValidationError({"items": f"Item {item_id} date {parsed} is before collection start date {collection.start_date}."})
+ if collection.end_date and parsed > collection.end_date:
+ raise ValidationError({"items": f"Item {item_id} date {parsed} is after collection end date {collection.end_date}."})
+
item.date = new_date
if new_order is not None:
item.order = new_order
diff --git a/backend/server/adventures/views/itinerary_view.py b/backend/server/adventures/views/itinerary_view.py
index 28f8eb43..c4c81af9 100644
--- a/backend/server/adventures/views/itinerary_view.py
+++ b/backend/server/adventures/views/itinerary_view.py
@@ -179,6 +179,34 @@ class ItineraryViewSet(viewsets.ModelViewSet):
item_date = data.get('date')
item_order = data.get('order', 0)
+ # Validate that the itinerary date (if provided) falls within the
+ # collection's start_date/end_date range (if those bounds are set).
+ if collection_id and item_date:
+ # Try parse date or datetime-like values
+ parsed_date = None
+ try:
+ parsed_date = parse_date(str(item_date))
+ except Exception:
+ parsed_date = None
+ if parsed_date is None:
+ try:
+ dt = parse_datetime(str(item_date))
+ if dt:
+ parsed_date = dt.date()
+ except Exception:
+ parsed_date = None
+
+ if parsed_date is not None:
+ try:
+ collection_obj = Collection.objects.get(id=collection_id)
+ except Collection.DoesNotExist:
+ return Response({'error': 'Collection not found'}, status=status.HTTP_404_NOT_FOUND)
+
+ if collection_obj.start_date and parsed_date < collection_obj.start_date:
+ return Response({'error': 'Itinerary item date is before the collection start_date'}, status=status.HTTP_400_BAD_REQUEST)
+ if collection_obj.end_date and parsed_date > collection_obj.end_date:
+ return Response({'error': 'Itinerary item date is after the collection end_date'}, status=status.HTTP_400_BAD_REQUEST)
+
if collection_id and item_date:
# Find the maximum order for this collection+date
existing_max = CollectionItineraryItem.objects.filter(
diff --git a/backend/server/adventures/views/location_view.py b/backend/server/adventures/views/location_view.py
index b7de9227..226c5c2e 100644
--- a/backend/server/adventures/views/location_view.py
+++ b/backend/server/adventures/views/location_view.py
@@ -7,7 +7,8 @@ from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
import requests
-from adventures.models import Location, Category
+from adventures.models import Location, Category, CollectionItineraryItem
+from django.contrib.contenttypes.models import ContentType
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from adventures.serializers import LocationSerializer, MapPinSerializer
from adventures.utils import pagination
@@ -277,6 +278,25 @@ class LocationViewSet(viewsets.ModelViewSet):
raise PermissionDenied(
f"You don't have permission to remove this location from one of the collections it's linked to.'"
)
+ else:
+ # If the removal is permitted, also remove any itinerary items
+ # in this collection that reference this Location instance.
+ try:
+ ct = ContentType.objects.get_for_model(instance.__class__)
+ # Try deleting by native PK type first, then by string.
+ qs = CollectionItineraryItem.objects.filter(
+ collection=collection, content_type=ct, object_id=instance.pk
+ )
+ if qs.exists():
+ qs.delete()
+ else:
+ CollectionItineraryItem.objects.filter(
+ collection=collection, content_type=ct, object_id=str(instance.pk)
+ ).delete()
+ except Exception:
+ # Don't raise on cleanup failures; deletion of itinerary items
+ # is best-effort and shouldn't block the update operation.
+ pass
def _validate_collection_permissions(self, collections):
"""Validate permissions for all collections (used in create)."""
diff --git a/frontend/src/lib/components/LocationLink.svelte b/frontend/src/lib/components/LocationLink.svelte
index 5da50e99..d69ee800 100644
--- a/frontend/src/lib/components/LocationLink.svelte
+++ b/frontend/src/lib/components/LocationLink.svelte
@@ -1,5 +1,5 @@
+
+{#if isOpen}
+
+{/if}
diff --git a/frontend/src/lib/components/transportation/TransportationDetails.svelte b/frontend/src/lib/components/transportation/TransportationDetails.svelte
index 1678eaba..0648a2f7 100644
--- a/frontend/src/lib/components/transportation/TransportationDetails.svelte
+++ b/frontend/src/lib/components/transportation/TransportationDetails.svelte
@@ -595,10 +595,6 @@
maxlength="5"
placeholder={airportMode ? 'JFK' : 'Code'}
/>
-
- {$t('transportation.autofill_code_hint') ||
- 'Auto-filled from airport search; you can override'}
-
diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts
index a25803f3..ede8b9e2 100644
--- a/frontend/src/lib/config.ts
+++ b/frontend/src/lib/config.ts
@@ -1,4 +1,4 @@
-export let appVersion = 'v0.12.0-pre-dev-122625';
+export let appVersion = 'v0.12.0-pre-dev-122725';
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.11.0';
export let appTitle = 'AdventureLog';
export let copyrightYear = '2023-2025';
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 63b63771..38445fc4 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -842,7 +842,9 @@
"enter_link": "Enter link",
"enter_flight_number": "Enter flight number",
"enter_from_location": "Enter from location",
- "enter_to_location": "Enter to location"
+ "enter_to_location": "Enter to location",
+ "arrival_code": "Arrival Code",
+ "departure_code": "Departure Code"
},
"lodging": {
"new_lodging": "New Lodging",
diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte
index d93d0268..bf4116c8 100644
--- a/frontend/src/routes/collections/[id]/+page.svelte
+++ b/frontend/src/routes/collections/[id]/+page.svelte
@@ -2,7 +2,7 @@
import type { Collection, ContentImage, Location } from '$lib/types';
import { onMount } from 'svelte';
import type { PageData } from './$types';
- import { goto } from '$app/navigation';
+ import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
import Lost from '$lib/assets/undraw_lost.svg';
import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre';
@@ -16,12 +16,15 @@
import CollectionAllItems from '$lib/components/collections/CollectionAllItems.svelte';
import CollectionItineraryPlanner from '$lib/components/collections/CollectionItineraryPlanner.svelte';
import CollectionRecommendationView from '$lib/components/CollectionRecommendationView.svelte';
+ import LocationLink from '$lib/components/LocationLink.svelte';
import { getBasemapUrl } from '$lib';
import FolderMultiple from '~icons/mdi/folder-multiple';
import FormatListBulleted from '~icons/mdi/format-list-bulleted';
import Timeline from '~icons/mdi/timeline';
import Map from '~icons/mdi/map';
import Lightbulb from '~icons/mdi/lightbulb';
+ import Plus from '~icons/mdi/plus';
+ import { addToast } from '$lib/toasts';
const renderMarkdown = (markdown: string) => {
return marked(markdown) as string;
@@ -37,6 +40,7 @@
let heroImages: ContentImage[] = [];
let modalInitialIndex: number = 0;
let isImageModalOpen: boolean = false;
+ let isLocationLinkModalOpen: boolean = false;
// View state from URL params
type ViewType = 'all' | 'itinerary' | 'map' | 'recommendations';
@@ -141,6 +145,64 @@
url.searchParams.set('view', view);
goto(url.toString(), { replaceState: true, noScroll: true });
}
+
+ function openLocationLinkModal() {
+ isLocationLinkModalOpen = true;
+ }
+
+ function closeLocationLinkModal() {
+ isLocationLinkModalOpen = false;
+ }
+
+ async function handleLocationAdded(event: CustomEvent) {
+ // Link the location to this collection
+ const location = event.detail;
+
+ try {
+ const response = await fetch(`/api/locations/${location.id}/`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ collections: [...(location.collections || []), collection.id]
+ })
+ });
+
+ if (response.ok) {
+ // Keep modal open so user can link more locations.
+ // Update local collection state so UI reflects the new link immediately.
+ try {
+ if (!collection.locations) collection.locations = [];
+ // Avoid duplicates
+ const exists = collection.locations.some((l) => String(l.id) === String(location.id));
+ if (!exists) {
+ collection.locations = [...collection.locations, location];
+ }
+ } catch (e) {
+ // if collection shape is unexpected, ignore and continue
+ console.warn('Unable to update local collection.locations', e);
+ }
+
+ // Show success message but do NOT close the modal or reload the page
+ addToast(
+ 'success',
+ $t('adventures.collection_link_location_success') || 'Location added successfully'
+ );
+ } else {
+ addToast(
+ 'error',
+ $t('adventures.collection_link_location_error') || 'Failed to add location'
+ );
+ }
+ } catch (error) {
+ console.error('Error linking location:', error);
+ addToast(
+ 'error',
+ $t('adventures.collection_link_location_error') || 'Failed to add location'
+ );
+ }
+ }
{#if notFound}
@@ -167,6 +229,15 @@
/>
{/if}
+{#if isLocationLinkModalOpen && collection}
+
+{/if}
+
{#if !collection && !notFound}
@@ -606,6 +677,19 @@
{/if}
+
+{#if collection && canModifyCollection}
+
+{/if}
+
{collection && collection.name ? `${collection.name}` : 'Collection'}