From 1b1d801151d4350211a45d3ea7da787cfa026419 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 8 Jan 2026 20:00:43 -0500 Subject: [PATCH] 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. --- .../server/adventures/views/itinerary_view.py | 68 +++++--- .../lib/components/cards/ChecklistCard.svelte | 18 ++- .../lib/components/cards/LocationCard.svelte | 144 +++++++++++++---- .../lib/components/cards/LodgingCard.svelte | 18 ++- .../src/lib/components/cards/NoteCard.svelte | 26 ++-- .../cards/TransportationCard.svelte | 18 ++- .../CollectionItineraryPlanner.svelte | 146 ++++++++++++------ .../collections/ItineraryDayPickModal.svelte | 2 +- .../collections/ItineraryLinkModal.svelte | 27 ++-- frontend/src/lib/config.ts | 2 +- frontend/src/locales/en.json | 3 +- frontend/src/routes/lodging/[id]/+page.svelte | 36 ++++- .../routes/transportations/[id]/+page.svelte | 72 +++++++++ 13 files changed, 423 insertions(+), 157 deletions(-) diff --git a/backend/server/adventures/views/itinerary_view.py b/backend/server/adventures/views/itinerary_view.py index 4e95d8ff..f6ddae34 100644 --- a/backend/server/adventures/views/itinerary_view.py +++ b/backend/server/adventures/views/itinerary_view.py @@ -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() diff --git a/frontend/src/lib/components/cards/ChecklistCard.svelte b/frontend/src/lib/components/cards/ChecklistCard.svelte index 3c9caa64..3f508190 100644 --- a/frontend/src/lib/components/cards/ChecklistCard.svelte +++ b/frontend/src/lib/components/cards/ChecklistCard.svelte @@ -283,20 +283,24 @@ {$t('itinerary.move_to_trip_context') || 'Move to Trip Context'} +
  • + +
  • {/if} -
  • - -
  • {/if} diff --git a/frontend/src/lib/components/cards/LocationCard.svelte b/frontend/src/lib/components/cards/LocationCard.svelte index aca321be..5124389e 100644 --- a/frontend/src/lib/components/cards/LocationCard.svelte +++ b/frontend/src/lib/components/cards/LocationCard.svelte @@ -1,5 +1,5 @@