mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-05-08 23:15:11 -04:00
feat: add functionality to change day and move items to trip-wide itinerary
- Implemented changeDay function in ChecklistCard, LocationCard, LodgingCard, NoteCard, and TransportationCard components to allow users to change the scheduled day of items. - Added a button to move items to the global (trip-wide) itinerary in the aforementioned components, with appropriate dispatch events. - Enhanced CollectionItineraryPlanner to handle moving items to the global itinerary and added UI elements for unscheduled items. - Updated ItineraryDayPickModal to support the deletion of source visits when moving locations. - Added new translations for "Change Day" and "Move Trip Wide" in the English locale.
This commit is contained in:
@@ -152,32 +152,91 @@ class ItineraryViewSet(viewsets.ModelViewSet):
|
||||
new_start = None
|
||||
new_end = None
|
||||
|
||||
# If we have valid bounds, check for any overlapping Visit for this location.
|
||||
# Overlap condition: existing.start_date <= new_end AND existing.end_date >= new_start
|
||||
# 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.
|
||||
if new_start and new_end:
|
||||
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
|
||||
source_visit_id = data.get('source_visit_id')
|
||||
if source_visit_id:
|
||||
try:
|
||||
source_visit = Visit.objects.get(id=source_visit_id, location=content_object)
|
||||
source_visit.start_date = new_start
|
||||
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
|
||||
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
|
||||
else:
|
||||
# For other item types, update their date field
|
||||
date_field = None
|
||||
if hasattr(content_object, 'date'):
|
||||
date_field = 'date'
|
||||
elif hasattr(content_object, 'start_date'):
|
||||
date_field = 'start_date'
|
||||
elif hasattr(content_object, 'check_in'):
|
||||
date_field = 'check_in'
|
||||
|
||||
if date_field:
|
||||
setattr(content_object, date_field, clean_date)
|
||||
content_object.save(update_fields=[date_field])
|
||||
# For other item types, update their date field and preserve duration
|
||||
if content_type_val == 'transportation':
|
||||
# For transportation: update date and end_date, preserving duration and times
|
||||
if hasattr(content_object, 'date') and hasattr(content_object, 'end_date'):
|
||||
old_date = content_object.date
|
||||
old_end_date = content_object.end_date
|
||||
|
||||
if old_date and old_end_date:
|
||||
# Extract time from original start date
|
||||
original_time = old_date.time()
|
||||
# Create new_date with the new date but preserve the original time
|
||||
new_date = datetime.datetime.combine(parse_date(clean_date), original_time)
|
||||
# Duration = end_date - date
|
||||
duration = old_end_date - old_date
|
||||
# Apply same duration to new date
|
||||
new_end_date = new_date + duration
|
||||
else:
|
||||
# No original end date, set to same as start date
|
||||
new_date = datetime.datetime.combine(parse_date(clean_date), datetime.time.min)
|
||||
new_end_date = new_date
|
||||
|
||||
content_object.date = new_date
|
||||
content_object.end_date = new_end_date
|
||||
content_object.save(update_fields=['date', 'end_date'])
|
||||
elif content_type_val == 'lodging':
|
||||
# For lodging: update check_in and check_out, preserving duration and times
|
||||
if hasattr(content_object, 'check_in') and hasattr(content_object, 'check_out'):
|
||||
old_check_in = content_object.check_in
|
||||
old_check_out = content_object.check_out
|
||||
|
||||
if old_check_in and old_check_out:
|
||||
# Extract time from original check_in
|
||||
original_time = old_check_in.time()
|
||||
# Create new_check_in with the new date but preserve the original time
|
||||
new_check_in = datetime.datetime.combine(parse_date(clean_date), original_time)
|
||||
# Duration = check_out - check_in
|
||||
duration = old_check_out - old_check_in
|
||||
# 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
|
||||
new_check_in = datetime.datetime.combine(parse_date(clean_date), datetime.time.min)
|
||||
new_check_out = new_check_in
|
||||
|
||||
content_object.check_in = new_check_in
|
||||
content_object.check_out = new_check_out
|
||||
content_object.save(update_fields=['check_in', 'check_out'])
|
||||
else:
|
||||
# For note, checklist, etc. - just update the date field
|
||||
date_field = None
|
||||
if hasattr(content_object, 'date'):
|
||||
date_field = 'date'
|
||||
elif hasattr(content_object, 'start_date'):
|
||||
date_field = 'start_date'
|
||||
|
||||
if date_field:
|
||||
setattr(content_object, date_field, clean_date)
|
||||
content_object.save(update_fields=[date_field])
|
||||
|
||||
# Ensure order is unique for this collection+group combination (day or global)
|
||||
collection_id = data.get('collection')
|
||||
@@ -246,8 +305,27 @@ class ItineraryViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
|
||||
# If we updated the item's date, include the updated object in response for frontend sync
|
||||
response_data = serializer.data
|
||||
if update_item_date and content_type_val and object_id:
|
||||
if content_type_val == 'transportation':
|
||||
try:
|
||||
t = Transportation.objects.get(id=object_id)
|
||||
from adventures.serializers import TransportationSerializer
|
||||
response_data['updated_object'] = TransportationSerializer(t).data
|
||||
except Transportation.DoesNotExist:
|
||||
pass
|
||||
elif content_type_val == 'lodging':
|
||||
try:
|
||||
l = Lodging.objects.get(id=object_id)
|
||||
from adventures.serializers import LodgingSerializer
|
||||
response_data['updated_object'] = LodgingSerializer(l).data
|
||||
except Lodging.DoesNotExist:
|
||||
pass
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
@transaction.atomic
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import CheckboxBlankCircleOutline from '~icons/mdi/checkbox-blank-circle-outline';
|
||||
import CalendarRemove from '~icons/mdi/calendar-remove';
|
||||
import Close from '~icons/mdi/close';
|
||||
import Globe from '~icons/mdi/globe';
|
||||
import type { CollectionItineraryItem } from '$lib/types';
|
||||
|
||||
export let checklist: Checklist;
|
||||
@@ -48,6 +49,10 @@
|
||||
dispatch('edit', checklist);
|
||||
}
|
||||
|
||||
function changeDay() {
|
||||
dispatch('changeDay', { type: 'checklist', item: checklist, forcePicker: true });
|
||||
}
|
||||
|
||||
async function deleteChecklist() {
|
||||
const res = await fetch(`/api/checklists/${checklist.id}`, {
|
||||
method: 'DELETE'
|
||||
@@ -267,6 +272,24 @@
|
||||
</li>
|
||||
{#if itineraryItem && itineraryItem.id}
|
||||
<div class="divider my-1"></div>
|
||||
{#if !itineraryItem.is_global}
|
||||
<li>
|
||||
<button
|
||||
on:click={() =>
|
||||
dispatch('moveToGlobal', { type: 'checklist', id: checklist.id })}
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Globe class="w-4 h-4" />
|
||||
{$t('itinerary.move_to_trip_wide') || 'Move to Trip-wide'}
|
||||
</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()}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import EyeOff from '~icons/mdi/eye-off';
|
||||
import CollectionItineraryPlanner from '../collections/CollectionItineraryPlanner.svelte';
|
||||
import CalendarRemove from '~icons/mdi/calendar-remove';
|
||||
import Globe from '~icons/mdi/globe';
|
||||
import { DEFAULT_CURRENCY, formatMoney, toMoneyValue } from '$lib/money';
|
||||
|
||||
export let type: string | null = null;
|
||||
@@ -93,6 +94,10 @@
|
||||
return stars;
|
||||
}
|
||||
|
||||
function changeDay() {
|
||||
dispatch('changeDay', { type: 'location', item: adventure, forcePicker: true });
|
||||
}
|
||||
|
||||
async function deleteAdventure() {
|
||||
let res = await fetch(`/api/locations/${adventure.id}`, {
|
||||
method: 'DELETE'
|
||||
@@ -373,6 +378,24 @@
|
||||
|
||||
{#if itineraryItem && itineraryItem.id}
|
||||
<div class="divider my-1"></div>
|
||||
{#if !itineraryItem.is_global}
|
||||
<li>
|
||||
<button
|
||||
on:click={() =>
|
||||
dispatch('moveToGlobal', { type: 'location', id: adventure.id })}
|
||||
class=" flex items-center gap-2"
|
||||
>
|
||||
<Globe class="w-4 h-4" />
|
||||
{$t('itinerary.move_to_trip_wide') || 'Move to Trip-wide'}
|
||||
</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()}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
import DotsHorizontal from '~icons/mdi/dots-horizontal';
|
||||
import CalendarRemove from '~icons/mdi/calendar-remove';
|
||||
import Launch from '~icons/mdi/launch';
|
||||
import Globe from '~icons/mdi/globe';
|
||||
import { goto } from '$app/navigation';
|
||||
import Calendar from '~icons/mdi/calendar';
|
||||
import type { CollectionItineraryItem } from '$lib/types';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -95,6 +97,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function changeDay() {
|
||||
dispatch('changeDay', { type: 'lodging', item: lodging, forcePicker: true });
|
||||
}
|
||||
|
||||
async function removeFromItinerary() {
|
||||
let itineraryItemId = itineraryItem?.id;
|
||||
let res = await fetch(`/api/itineraries/${itineraryItemId}`, {
|
||||
@@ -193,6 +199,23 @@
|
||||
</li>
|
||||
{#if itineraryItem && itineraryItem.id}
|
||||
<div class="divider my-1"></div>
|
||||
{#if !itineraryItem.is_global}
|
||||
<li>
|
||||
<button
|
||||
on:click={() => dispatch('moveToGlobal', { type: 'lodging', id: lodging.id })}
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Globe class="w-4 h-4" />
|
||||
{$t('itinerary.move_to_trip_wide') || 'Move to Trip-wide'}
|
||||
</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()}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import CalendarRemove from '~icons/mdi/calendar-remove';
|
||||
import Launch from '~icons/mdi/launch';
|
||||
import Close from '~icons/mdi/close';
|
||||
import Globe from '~icons/mdi/globe';
|
||||
import type { CollectionItineraryItem } from '$lib/types';
|
||||
|
||||
export let note: Note;
|
||||
@@ -53,6 +54,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function changeDay() {
|
||||
dispatch('changeDay', { type: 'note', item: note, forcePicker: true });
|
||||
}
|
||||
|
||||
async function removeFromItinerary() {
|
||||
let itineraryItemId = itineraryItem?.id;
|
||||
let res = await fetch(`/api/itineraries/${itineraryItemId}`, {
|
||||
@@ -189,6 +194,23 @@
|
||||
</li>
|
||||
{#if itineraryItem && itineraryItem.id}
|
||||
<div class="divider my-1"></div>
|
||||
{#if !itineraryItem.is_global}
|
||||
<li>
|
||||
<button
|
||||
on:click={() => dispatch('moveToGlobal', { type: 'note', id: note.id })}
|
||||
class="text-info flex items-center gap-2"
|
||||
>
|
||||
<Globe class="w-4 h-4 text-info" />
|
||||
{$t('itinerary.move_to_trip_wide') || 'Move to Trip-wide'}
|
||||
</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()}
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
import EyeOff from '~icons/mdi/eye-off';
|
||||
import Star from '~icons/mdi/star';
|
||||
import StarOutline from '~icons/mdi/star-outline';
|
||||
import Calendar from '~icons/mdi/calendar';
|
||||
import DotsHorizontal from '~icons/mdi/dots-horizontal';
|
||||
import CalendarRemove from '~icons/mdi/calendar-remove';
|
||||
import Launch from '~icons/mdi/launch';
|
||||
import Globe from '~icons/mdi/globe';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { CollectionItineraryItem } from '$lib/types';
|
||||
|
||||
@@ -72,6 +74,10 @@
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
function changeDay() {
|
||||
dispatch('changeDay', { type: 'transportation', item: transportation, forcePicker: true });
|
||||
}
|
||||
|
||||
let travelDurationLabel: string | null = null;
|
||||
$: travelDurationLabel = formatTravelDuration(transportation?.travel_duration_minutes ?? null);
|
||||
|
||||
@@ -226,6 +232,24 @@
|
||||
</li>
|
||||
{#if itineraryItem && itineraryItem.id}
|
||||
<div class="divider my-1"></div>
|
||||
{#if !itineraryItem.is_global}
|
||||
<li>
|
||||
<button
|
||||
on:click={() =>
|
||||
dispatch('moveToGlobal', { type: 'transportation', id: transportation.id })}
|
||||
class=" flex items-center gap-2"
|
||||
>
|
||||
<Globe class="w-4 h-4 " />
|
||||
{$t('itinerary.move_to_trip_wide') || 'Move to Trip-wide'}
|
||||
</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()}
|
||||
@@ -369,12 +393,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if travelDurationLabel}
|
||||
<div class="flex items-center gap-2 text-xs text-base-content/70">
|
||||
<span class="badge badge-ghost badge-xs">⏱️ {travelDurationLabel}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
import ItineraryLinkModal from '$lib/components/collections/ItineraryLinkModal.svelte';
|
||||
import ItineraryDayPickModal from '$lib/components/collections/ItineraryDayPickModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import Globe from '~icons/mdi/globe';
|
||||
|
||||
export let collection: Collection;
|
||||
export let user: any;
|
||||
@@ -156,6 +158,118 @@
|
||||
pendingAddDate = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an item to the global (trip-wide) itinerary.
|
||||
* Removes all dated entries for this item and adds it to the global view instead.
|
||||
*/
|
||||
async function moveItemToGlobal(objectType: string, objectId: string) {
|
||||
if (!collection.id) return;
|
||||
|
||||
try {
|
||||
// Remove all dated itinerary entries for this item
|
||||
const entriesToRemove = (collection.itinerary || [])
|
||||
.filter((it) => it.object_id === objectId && it.date && !it.is_global)
|
||||
.map((it) => it.id);
|
||||
|
||||
// Delete the dated entries
|
||||
for (const entryId of entriesToRemove) {
|
||||
await fetch(`/api/itineraries/${entryId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Add as global if not already there
|
||||
const alreadyGlobal = (collection.itinerary || []).some(
|
||||
(it) => it.object_id === objectId && it.is_global
|
||||
);
|
||||
|
||||
if (!alreadyGlobal) {
|
||||
const order = globalItems.length;
|
||||
const res = await fetch('/api/itineraries/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
collection: collection.id,
|
||||
content_type: objectType,
|
||||
object_id: objectId,
|
||||
is_global: true,
|
||||
order
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to add to global itinerary');
|
||||
const created = await res.json();
|
||||
collection.itinerary = [...(collection.itinerary || []), created];
|
||||
}
|
||||
|
||||
// Remove dated entries from local state
|
||||
collection.itinerary = (collection.itinerary || []).filter(
|
||||
(it) => !entriesToRemove.includes(it.id)
|
||||
);
|
||||
|
||||
// Refresh reactive variables
|
||||
days = groupItemsByDay(collection);
|
||||
globalItems = (collection.itinerary || [])
|
||||
.filter((it) => it.is_global)
|
||||
.map((it) => resolveItineraryItem(it, collection))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
|
||||
addToast('success', `Moved to trip-wide items`);
|
||||
} catch (error) {
|
||||
console.error('Error moving item to global:', error);
|
||||
addToast('error', 'Failed to move item to trip-wide');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an unscheduled item directly to the global (trip-wide) itinerary.
|
||||
*/
|
||||
async function addUnscheduledItemToGlobal(type: string, itemId: string) {
|
||||
if (!collection.id) return;
|
||||
|
||||
try {
|
||||
// Check if already in global view
|
||||
const alreadyGlobal = (collection.itinerary || []).some(
|
||||
(it) => it.object_id === itemId && it.is_global
|
||||
);
|
||||
|
||||
if (alreadyGlobal) {
|
||||
addToast('info', 'Item already in trip-wide view');
|
||||
return;
|
||||
}
|
||||
|
||||
const order = globalItems.length;
|
||||
const res = await fetch('/api/itineraries/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
collection: collection.id,
|
||||
content_type: type,
|
||||
object_id: itemId,
|
||||
is_global: true,
|
||||
order
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to add to global itinerary');
|
||||
|
||||
const created = await res.json();
|
||||
collection.itinerary = [...(collection.itinerary || []), created];
|
||||
|
||||
// Remove from unscheduled by moving it to itinerary
|
||||
unscheduledItems = unscheduledItems.filter((item) => !(item.item.id === itemId));
|
||||
|
||||
// Refresh global items
|
||||
globalItems = (collection.itinerary || [])
|
||||
.filter((it) => it.is_global)
|
||||
.map((it) => resolveItineraryItem(it, collection))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
|
||||
addToast('success', `Added to trip-wide items`);
|
||||
} catch (error) {
|
||||
console.error('Error adding item to global:', error);
|
||||
addToast('error', 'Failed to add item to trip-wide');
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemDelete(event: CustomEvent<CollectionItineraryItem | string | number>) {
|
||||
const payload = event.detail;
|
||||
|
||||
@@ -239,6 +353,8 @@
|
||||
// Day picker modal state for unscheduled items
|
||||
let isDayPickModalOpen = false;
|
||||
let dayPickItemToAdd: { type: string; item: any } | null = null;
|
||||
let dayPickScheduledDates: string[] = [];
|
||||
let dayPickSourceVisit: { id: string; start_date: string } | null = null;
|
||||
|
||||
// When opening a "create new item" modal we store the target date here
|
||||
let pendingAddDate: string | null = null;
|
||||
@@ -835,16 +951,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Handle opening the day picker modal for an unscheduled item
|
||||
function handleOpenDayPickerForItem(type: string, item: any) {
|
||||
// Handle opening the day picker modal for an item (scheduled or unscheduled)
|
||||
// currentItineraryDate: the date of the itinerary entry being moved (if any)
|
||||
function handleOpenDayPickerForItem(
|
||||
type: string,
|
||||
item: any,
|
||||
forcePicker: boolean = false,
|
||||
currentItineraryDate: string | null = null
|
||||
) {
|
||||
// Check if the item already has a date, and if so, add it directly
|
||||
let itemDate: string | null = null;
|
||||
// Track all itinerary dates this item is already scheduled on (non-global)
|
||||
const scheduledDates = (collection.itinerary || [])
|
||||
.filter((it) => it.object_id === item.id && it.date && !it.is_global)
|
||||
.map((it) => it.date as string);
|
||||
|
||||
if (type === 'location') {
|
||||
// For locations, check if there's a visit with a start_date
|
||||
const firstVisit = item.visits?.[0];
|
||||
// For locations, prefer the visit matching the current itinerary date
|
||||
let matchedVisit = null;
|
||||
if (currentItineraryDate) {
|
||||
matchedVisit = item.visits?.find((v) => v.start_date?.startsWith(currentItineraryDate));
|
||||
}
|
||||
const firstVisit = matchedVisit || item.visits?.[0];
|
||||
if (firstVisit?.start_date) {
|
||||
itemDate = firstVisit.start_date.split('T')[0]; // Extract date only (YYYY-MM-DD)
|
||||
dayPickSourceVisit = { id: firstVisit.id, start_date: firstVisit.start_date };
|
||||
}
|
||||
} else if (type === 'transportation') {
|
||||
if (item.date) {
|
||||
@@ -864,6 +995,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
// If caller explicitly wants the picker, bypass auto-add
|
||||
if (forcePicker) {
|
||||
dayPickItemToAdd = { type, item };
|
||||
dayPickScheduledDates = scheduledDates;
|
||||
// Capture source visit for locations (match itinerary date first)
|
||||
dayPickSourceVisit = null;
|
||||
if (type === 'location') {
|
||||
const matchedVisit = currentItineraryDate
|
||||
? item.visits?.find((v) => v.start_date?.startsWith(currentItineraryDate))
|
||||
: null;
|
||||
const firstVisit = matchedVisit || item.visits?.[0];
|
||||
if (firstVisit?.start_date) {
|
||||
dayPickSourceVisit = { id: firstVisit.id, start_date: firstVisit.start_date };
|
||||
}
|
||||
}
|
||||
isDayPickModalOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we found a date, add it directly to that date
|
||||
// Helper: check if a date is within collection start/end bounds (if set)
|
||||
function isDateWithinCollectionRange(dateISO: string | null) {
|
||||
@@ -889,33 +1039,120 @@
|
||||
// If the item's date is outside the collection range, prompt the day picker
|
||||
if (!isDateWithinCollectionRange(itemDate)) {
|
||||
dayPickItemToAdd = { type, item };
|
||||
dayPickScheduledDates = scheduledDates;
|
||||
dayPickSourceVisit = null;
|
||||
if (type === 'location') {
|
||||
// Prefer the visit that matches itemDate if present
|
||||
const source = item.visits?.find((v) => v.start_date?.startsWith(itemDate));
|
||||
const useVisit = source || item.visits?.[0];
|
||||
if (useVisit?.start_date) {
|
||||
dayPickSourceVisit = { id: useVisit.id, start_date: useVisit.start_date };
|
||||
}
|
||||
}
|
||||
isDayPickModalOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// We have a valid date; ensure dayPickSourceVisit is aligned for locations with multiple visits
|
||||
if (type === 'location' && !dayPickSourceVisit) {
|
||||
const source = item.visits?.find((v) => v.start_date?.startsWith(itemDate));
|
||||
if (source?.start_date) {
|
||||
dayPickSourceVisit = { id: source.id, start_date: source.start_date };
|
||||
}
|
||||
}
|
||||
|
||||
addItineraryItemForObject(type, item.id, itemDate, false);
|
||||
} else {
|
||||
// Otherwise, show the day picker modal
|
||||
dayPickItemToAdd = { type, item };
|
||||
dayPickScheduledDates = scheduledDates;
|
||||
dayPickSourceVisit = null;
|
||||
if (type === 'location') {
|
||||
const matchedVisit = currentItineraryDate
|
||||
? item.visits?.find((v) => v.start_date?.startsWith(currentItineraryDate))
|
||||
: item.visits?.[0];
|
||||
if (matchedVisit?.start_date) {
|
||||
dayPickSourceVisit = { id: matchedVisit.id, start_date: matchedVisit.start_date };
|
||||
}
|
||||
}
|
||||
isDayPickModalOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle day selection from the day picker modal
|
||||
async function handleDaySelected(event: CustomEvent<{ date: string; updateDate: boolean }>) {
|
||||
const { date: selectedDate, updateDate } = event.detail;
|
||||
async function handleDaySelected(
|
||||
event: CustomEvent<{ date: string; updateDate: boolean; deleteSourceVisit?: boolean }>
|
||||
) {
|
||||
const { date: selectedDate, updateDate, deleteSourceVisit } = event.detail;
|
||||
if (!dayPickItemToAdd) return;
|
||||
|
||||
const { type, item } = dayPickItemToAdd;
|
||||
const objectType = type; // 'location', 'transportation', 'lodging', 'note', 'checklist'
|
||||
const objectId = item.id;
|
||||
|
||||
// Add the item to the selected day
|
||||
await addItineraryItemForObject(objectType, objectId, selectedDate, updateDate);
|
||||
// Identify existing dated itinerary entries for this object
|
||||
const existingDatedItems = (collection.itinerary || []).filter(
|
||||
(it) => it.object_id === objectId && it.date && !it.is_global
|
||||
);
|
||||
|
||||
// Reset state
|
||||
dayPickItemToAdd = null;
|
||||
isDayPickModalOpen = false;
|
||||
// Avoid duplicate add if already scheduled for the selected date
|
||||
const alreadyScheduledForSelectedDate = existingDatedItems.some(
|
||||
(it) => it.date === selectedDate
|
||||
);
|
||||
|
||||
try {
|
||||
if (!alreadyScheduledForSelectedDate) {
|
||||
// Add the item to the selected day
|
||||
await addItineraryItemForObject(objectType, objectId, selectedDate, updateDate);
|
||||
}
|
||||
|
||||
// Optionally delete the source visit (for locations) — skip if we're updating it
|
||||
if (deleteSourceVisit && objectType === 'location' && dayPickSourceVisit?.id && !updateDate) {
|
||||
try {
|
||||
await fetch(`/api/visits/${dayPickSourceVisit.id}/`, { method: 'DELETE' });
|
||||
// Update local state: remove visit from the location
|
||||
if (collection.locations) {
|
||||
collection.locations = collection.locations.map((loc) => {
|
||||
if (loc.id === objectId) {
|
||||
return {
|
||||
...loc,
|
||||
visits: (loc.visits || []).filter((v) => v.id !== dayPickSourceVisit?.id)
|
||||
};
|
||||
}
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to delete source visit', dayPickSourceVisit.id, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
await fetch(`/api/itineraries/${oldItem.id}`, { method: 'DELETE' });
|
||||
} catch (e) {
|
||||
console.error('Failed to remove old itinerary item', oldItem.id, 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;
|
||||
isDayPickModalOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add an itinerary item locally and attempt to persist to backend
|
||||
@@ -953,7 +1190,12 @@
|
||||
object_id: objectId,
|
||||
date: dateISO,
|
||||
order,
|
||||
update_item_date: updateItemDate
|
||||
update_item_date: updateItemDate,
|
||||
// Prefer updating an existing Visit when moving a location
|
||||
source_visit_id:
|
||||
objectType === 'location' && updateItemDate && dayPickSourceVisit?.id
|
||||
? dayPickSourceVisit.id
|
||||
: undefined
|
||||
})
|
||||
});
|
||||
|
||||
@@ -966,89 +1208,58 @@
|
||||
collection.itinerary = collection.itinerary.map((it) => (it.id === tempId ? created : it));
|
||||
pendingAddDate = null;
|
||||
|
||||
// If we updated the item's date, update local state directly
|
||||
if (updateItemDate) {
|
||||
// If we updated the item's date, sync the updated object from server response
|
||||
if (updateItemDate && created.updated_object) {
|
||||
if (objectType === 'transportation') {
|
||||
if (collection.transportations) {
|
||||
collection.transportations = collection.transportations.map((t) =>
|
||||
t.id === objectId ? { ...t, ...created.updated_object } : t
|
||||
);
|
||||
}
|
||||
} else if (objectType === 'lodging') {
|
||||
if (collection.lodging) {
|
||||
collection.lodging = collection.lodging.map((l) =>
|
||||
l.id === objectId ? { ...l, ...created.updated_object } : l
|
||||
);
|
||||
}
|
||||
}
|
||||
} 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, create a new visit locally
|
||||
// For locations, update existing source visit when available; otherwise append a new visit
|
||||
const sourceId = dayPickSourceVisit?.id;
|
||||
if (collection.locations) {
|
||||
collection.locations = collection.locations.map((loc) => {
|
||||
if (loc.id === objectId) {
|
||||
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]
|
||||
};
|
||||
}
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
} else if (objectType === 'transportation') {
|
||||
if (collection.transportations) {
|
||||
collection.transportations = collection.transportations.map((t) => {
|
||||
if (t.id === objectId) {
|
||||
// If end_date exists and is before the new start date, set it to next day
|
||||
let newEndDate = t.end_date;
|
||||
if (newEndDate) {
|
||||
const endDate = DateTime.fromISO(newEndDate);
|
||||
const startDate = DateTime.fromISO(isoDate);
|
||||
if (endDate < startDate) {
|
||||
// Check if original end_date has a time component (not all-day)
|
||||
const hasTime = !newEndDate.includes('T00:00:00');
|
||||
if (hasTime && t.end_timezone) {
|
||||
// Set to 9am in the end timezone
|
||||
newEndDate = DateTime.fromISO(nextDayISO, { zone: t.end_timezone })
|
||||
.set({ hour: 9, minute: 0, second: 0 })
|
||||
.toISO();
|
||||
} else {
|
||||
// All-day event, keep at UTC 0
|
||||
newEndDate = `${nextDayISO}T00:00:00`;
|
||||
}
|
||||
}
|
||||
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 };
|
||||
}
|
||||
return { ...t, date: isoDate, end_date: newEndDate };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}
|
||||
} else if (objectType === 'lodging') {
|
||||
if (collection.lodging) {
|
||||
collection.lodging = collection.lodging.map((l) => {
|
||||
if (l.id === objectId) {
|
||||
// If check_out exists and is before the new check_in, set it to next day
|
||||
let newCheckOut = l.check_out;
|
||||
if (newCheckOut) {
|
||||
const checkOut = DateTime.fromISO(newCheckOut);
|
||||
const checkIn = DateTime.fromISO(isoDate);
|
||||
if (checkOut < checkIn) {
|
||||
// Check if original check_out has a time component (not all-day)
|
||||
const hasTime = !newCheckOut.includes('T00:00:00');
|
||||
if (hasTime && l.timezone) {
|
||||
// Set to 9am in the lodging timezone
|
||||
newCheckOut = DateTime.fromISO(nextDayISO, { zone: l.timezone })
|
||||
.set({ hour: 9, minute: 0, second: 0 })
|
||||
.toISO();
|
||||
} else {
|
||||
// All-day event, keep at UTC 0
|
||||
newCheckOut = `${nextDayISO}T00:00:00`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ...l, check_in: isoDate, check_out: newCheckOut };
|
||||
}
|
||||
return l;
|
||||
|
||||
// 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] };
|
||||
});
|
||||
}
|
||||
} else if (objectType === 'note') {
|
||||
@@ -1241,10 +1452,14 @@
|
||||
isOpen={isDayPickModalOpen}
|
||||
{days}
|
||||
itemName={dayPickItemToAdd?.item?.name || `${$t('checklist.item')}`}
|
||||
scheduledDates={dayPickScheduledDates}
|
||||
sourceVisitDate={dayPickSourceVisit ? dayPickSourceVisit.start_date.split('T')[0] : null}
|
||||
on:daySelected={handleDaySelected}
|
||||
on:close={() => {
|
||||
isDayPickModalOpen = false;
|
||||
dayPickItemToAdd = null;
|
||||
dayPickScheduledDates = [];
|
||||
dayPickSourceVisit = null;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -1314,12 +1529,40 @@
|
||||
<Plus class="w-5 h-5" />
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-content menu p-2 shadow bg-base-300 rounded-box w-56"
|
||||
class="dropdown-content menu p-2 shadow bg-base-300 rounded-box w-72"
|
||||
role="menu"
|
||||
>
|
||||
<li class="menu-title">{$t('itinerary.link_existing_item')}</li>
|
||||
{#if unscheduledItems.length > 0}
|
||||
<li class="menu-title">
|
||||
<span>{$t('itinerary.add_to_trip_wide') || 'Add Unscheduled Items'}</span>
|
||||
</li>
|
||||
{#each unscheduledItems as unscheduledItem (unscheduledItem.item.id)}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
addUnscheduledItemToGlobal(
|
||||
unscheduledItem.type,
|
||||
unscheduledItem.item.id
|
||||
);
|
||||
}}
|
||||
class="text-xs py-1"
|
||||
>
|
||||
<span class="truncate">{unscheduledItem.item.name}</span>
|
||||
<span class="badge badge-xs badge-ghost">
|
||||
{unscheduledItem.type}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
<li class="divider my-1"></li>
|
||||
{/if}
|
||||
<li class="text-xs opacity-70 px-2 py-1 select-none">
|
||||
Add items below (Unscheduled) to trip-wide
|
||||
{#if unscheduledItems.length === 0}
|
||||
All items are scheduled or in trip-wide view
|
||||
{:else}
|
||||
Or drag items from unscheduled below
|
||||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1392,6 +1635,7 @@
|
||||
on:delete={handleItemDelete}
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
{user}
|
||||
{collection}
|
||||
compact={true}
|
||||
@@ -1405,6 +1649,7 @@
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:edit={handleEditTransportation}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
/>
|
||||
{:else if objectType === 'lodging'}
|
||||
<LodgingCard
|
||||
@@ -1415,6 +1660,7 @@
|
||||
on:delete={handleItemDelete}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:edit={handleEditLodging}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
/>
|
||||
{:else if objectType === 'note'}
|
||||
<NoteCard
|
||||
@@ -1425,6 +1671,7 @@
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:edit={handleEditNote}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
/>
|
||||
{:else if objectType === 'checklist'}
|
||||
<ChecklistCard
|
||||
@@ -1435,6 +1682,7 @@
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:edit={handleEditChecklist}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -1797,9 +2045,17 @@
|
||||
on:delete={handleItemDelete}
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
{user}
|
||||
{collection}
|
||||
compact={true}
|
||||
on:changeDay={(e) =>
|
||||
handleOpenDayPickerForItem(
|
||||
e.detail.type,
|
||||
e.detail.item,
|
||||
e.detail.forcePicker,
|
||||
day.date
|
||||
)}
|
||||
/>
|
||||
{:else if objectType === 'transportation'}
|
||||
<TransportationCard
|
||||
@@ -1810,6 +2066,13 @@
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:edit={handleEditTransportation}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
on:changeDay={(e) =>
|
||||
handleOpenDayPickerForItem(
|
||||
e.detail.type,
|
||||
e.detail.item,
|
||||
e.detail.forcePicker
|
||||
)}
|
||||
/>
|
||||
{:else if objectType === 'lodging'}
|
||||
<LodgingCard
|
||||
@@ -1820,6 +2083,13 @@
|
||||
on:delete={handleItemDelete}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:edit={handleEditLodging}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
on:changeDay={(e) =>
|
||||
handleOpenDayPickerForItem(
|
||||
e.detail.type,
|
||||
e.detail.item,
|
||||
e.detail.forcePicker
|
||||
)}
|
||||
/>
|
||||
{:else if objectType === 'note'}
|
||||
<!-- @ts-ignore - TypeScript can't narrow union type properly -->
|
||||
@@ -1831,6 +2101,13 @@
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:edit={handleEditNote}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
on:changeDay={(e) =>
|
||||
handleOpenDayPickerForItem(
|
||||
e.detail.type,
|
||||
e.detail.item,
|
||||
e.detail.forcePicker
|
||||
)}
|
||||
/>
|
||||
{:else if objectType === 'checklist'}
|
||||
<!-- @ts-ignore - TypeScript can't narrow union type properly -->
|
||||
@@ -1842,6 +2119,13 @@
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
on:edit={handleEditChecklist}
|
||||
on:moveToGlobal={(e) => moveItemToGlobal(e.detail.type, e.detail.id)}
|
||||
on:changeDay={(e) =>
|
||||
handleOpenDayPickerForItem(
|
||||
e.detail.type,
|
||||
e.detail.item,
|
||||
e.detail.forcePicker
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// @ts-ignore
|
||||
import { DateTime } from 'luxon';
|
||||
// lodging icons and helpers
|
||||
import { LODGING_TYPES_ICONS } from '$lib';
|
||||
import { LODGING_TYPES_ICONS, getActivityIcon, SPORT_TYPE_CHOICES } from '$lib';
|
||||
|
||||
export let collection: Collection;
|
||||
export let user: User | null = null;
|
||||
@@ -270,6 +270,12 @@
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
function getSportLabel(key: string): string {
|
||||
const found = (SPORT_TYPE_CHOICES as Array<any>).find((a) => a.key === key);
|
||||
if (found) return found.label;
|
||||
return String(key).replace(/([a-z])([A-Z])/g, '$1 $2');
|
||||
}
|
||||
|
||||
$: distanceByTransportType = (() => {
|
||||
const types = new Map<string, number>();
|
||||
transportSegments.forEach((segment) => {
|
||||
@@ -566,9 +572,11 @@
|
||||
<h4 class="font-semibold mb-2">Sport Types</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each sportTypes as [sport, count]}
|
||||
<span class="badge badge-lg badge-primary badge-outline">
|
||||
{sport} ({count})
|
||||
</span>
|
||||
<div class="badge badge-lg badge-primary badge-outline p-3 flex items-center gap-2">
|
||||
<span class="text-lg">{getActivityIcon(sport)}</span>
|
||||
<span class="font-medium">{getSportLabel(sport)}</span>
|
||||
<span class="badge badge-xs badge-primary ml-2">{count}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
export let isOpen: boolean = false;
|
||||
export let days: Array<{ date: string; displayDate: string; items: any[] }> = [];
|
||||
export let itemName: string = 'Item';
|
||||
export let scheduledDates: string[] = [];
|
||||
// Optional: source visit info when moving a dated location
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let deleteSourceVisit: boolean = false;
|
||||
|
||||
function handleDaySelect(dayDate: string, updateDate: boolean) {
|
||||
dispatch('daySelected', { date: dayDate, updateDate });
|
||||
dispatch('daySelected', { date: dayDate, updateDate, deleteSourceVisit });
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
@@ -68,6 +72,7 @@
|
||||
{@const weekday = DateTime.fromISO(day.date).toFormat('ccc')}
|
||||
{@const dayOfMonth = DateTime.fromISO(day.date).toFormat('d')}
|
||||
{@const monthAbbrev = DateTime.fromISO(day.date).toFormat('LLL')}
|
||||
{@const isScheduled = scheduledDates?.includes(day.date)}
|
||||
|
||||
<div
|
||||
class="card bg-base-100 border border-base-300 shadow-sm hover:border-primary/60 hover:shadow-md transition-all"
|
||||
@@ -86,6 +91,11 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="badge badge-primary badge-sm">Day {dayNumber}</span>
|
||||
<span class="text-xs opacity-60">of {totalDays}</span>
|
||||
{#if isScheduled}
|
||||
<span class="badge badge-neutral badge-outline badge-sm"
|
||||
>Already scheduled</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="font-semibold text-base mt-1">{day.displayDate}</div>
|
||||
<div class="text-sm opacity-70 flex items-center gap-2 mt-1">
|
||||
@@ -97,19 +107,13 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm flex-1"
|
||||
on:click={() => handleDaySelect(day.date, false)}
|
||||
>
|
||||
Add as is
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm flex-1"
|
||||
disabled={isScheduled}
|
||||
on:click={() => handleDaySelect(day.date, true)}
|
||||
>
|
||||
Add & Update Date
|
||||
{isScheduled ? 'Already scheduled' : 'Update to this day'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1104,7 +1104,9 @@
|
||||
"item_not_found": "Item not found",
|
||||
"staying_overnight": "Staying overnight",
|
||||
"unscheduled_items": "Unscheduled Items",
|
||||
"unscheduled_items_desc": "These items are linked to this trip but haven't been added to a specific day yet."
|
||||
"unscheduled_items_desc": "These items are linked to this trip but haven't been added to a specific day yet.",
|
||||
"change_day": "Change Day",
|
||||
"move_to_trip_wide": "Move Trip Wide"
|
||||
},
|
||||
"common": {
|
||||
"show_less": "Hide details",
|
||||
|
||||
Reference in New Issue
Block a user