mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-12-23 22:58:17 -05:00
feat: enhance itinerary management with deduplication and initial visit date handling
This commit is contained in:
@@ -8,8 +8,7 @@
|
||||
|
||||
<!-- iOS / Safari PWA support -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<!-- Apple touch icons (place files in frontend/static/) -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||
<link
|
||||
|
||||
@@ -170,8 +170,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Store results to display inline
|
||||
wikiImageResults = data.images;
|
||||
// Store results to display inline (deduplicated by source)
|
||||
{
|
||||
const seen = new Set();
|
||||
wikiImageResults = (data.images || []).filter((img: { source: unknown }) => {
|
||||
if (!img || !img.source) return false;
|
||||
if (seen.has(img.source)) return false;
|
||||
seen.add(img.source);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
wikiImageError = $t('adventures.wiki_image_error');
|
||||
addToast('error', $t('adventures.image_upload_error'));
|
||||
@@ -366,7 +374,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 max-h-96 overflow-y-auto">
|
||||
{#each wikiImageResults as result (result.source)}
|
||||
{#each wikiImageResults as result, i (result.source + '-' + i)}
|
||||
<button
|
||||
type="button"
|
||||
class="card bg-base-100 border border-base-300 hover:border-primary hover:shadow-lg transition-all duration-200 cursor-pointer group"
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
export let user: User | null = null;
|
||||
export let collection: Collection | null = null;
|
||||
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
|
||||
export let initialVisitDate: string | null = null; // Used to pre-fill visit date when adding from itinerary planner
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Store the initial visit date internally so it persists even if parent clears it
|
||||
let storedInitialVisitDate: string | null = initialVisitDate;
|
||||
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
let steps = [
|
||||
@@ -185,7 +189,7 @@
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5-5z"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-0.089l4-5-5z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
@@ -321,6 +325,7 @@
|
||||
on:close={() => close()}
|
||||
measurementSystem={user?.measurement_system || 'metric'}
|
||||
{collection}
|
||||
initialVisitDate={storedInitialVisitDate}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -119,13 +119,69 @@
|
||||
isLocationModalOpen = true;
|
||||
}
|
||||
|
||||
function handleDeleteLocation(event: CustomEvent<Location>) {
|
||||
// remove locally deleted location from itinerary view and list
|
||||
const deletedLocation = event.detail;
|
||||
collection.locations = collection.locations?.filter((loc) => loc.id !== deletedLocation.id);
|
||||
collection.itinerary = collection.itinerary?.filter(
|
||||
(it) => !(it.item?.type === 'location' && it.object_id === deletedLocation.id)
|
||||
);
|
||||
function handleItemDelete(event: CustomEvent<CollectionItineraryItem | string | number>) {
|
||||
const payload = event.detail;
|
||||
|
||||
// Support both cases:
|
||||
// 1) Card components dispatch a primitive id (string/number) when deleting the underlying object
|
||||
// 2) Some callers may dispatch a full itinerary item object
|
||||
if (typeof payload === 'string' || typeof payload === 'number') {
|
||||
const objectId = payload;
|
||||
|
||||
// Remove any itinerary entries that reference this object
|
||||
collection.itinerary = collection.itinerary?.filter(
|
||||
(it) => String(it.object_id) !== String(objectId)
|
||||
);
|
||||
|
||||
// Remove the object from all possible collections (location/transportation/lodging/note/checklist)
|
||||
if (collection.locations) {
|
||||
collection.locations = collection.locations.filter(
|
||||
(loc) => String(loc.id) !== String(objectId)
|
||||
);
|
||||
}
|
||||
if (collection.transportations) {
|
||||
collection.transportations = collection.transportations.filter(
|
||||
(t) => String(t.id) !== String(objectId)
|
||||
);
|
||||
}
|
||||
if (collection.lodging) {
|
||||
collection.lodging = collection.lodging.filter((l) => String(l.id) !== String(objectId));
|
||||
}
|
||||
if (collection.notes) {
|
||||
collection.notes = collection.notes.filter((n) => String(n.id) !== String(objectId));
|
||||
}
|
||||
if (collection.checklists) {
|
||||
collection.checklists = collection.checklists.filter(
|
||||
(c) => String(c.id) !== String(objectId)
|
||||
);
|
||||
}
|
||||
|
||||
// Re-group days and return
|
||||
days = groupItemsByDay(collection);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise expect a full itinerary-like object
|
||||
const itemToDelete = payload as CollectionItineraryItem;
|
||||
collection.itinerary = collection.itinerary?.filter((it) => it.id !== itemToDelete.id);
|
||||
// Also remove the associated object from the collection
|
||||
const objectType = itemToDelete.item?.type || '';
|
||||
if (objectType === 'location') {
|
||||
collection.locations = collection.locations?.filter(
|
||||
(loc) => loc.id !== itemToDelete.object_id
|
||||
);
|
||||
} else if (objectType === 'transportation') {
|
||||
collection.transportations = collection.transportations?.filter(
|
||||
(t) => t.id !== itemToDelete.object_id
|
||||
);
|
||||
} else if (objectType === 'lodging') {
|
||||
collection.lodging = collection.lodging?.filter((l) => l.id !== itemToDelete.object_id);
|
||||
} else if (objectType === 'note') {
|
||||
collection.notes = collection.notes?.filter((n) => n.id !== itemToDelete.object_id);
|
||||
} else if (objectType === 'checklist') {
|
||||
collection.checklists = collection.checklists?.filter((c) => c.id !== itemToDelete.object_id);
|
||||
}
|
||||
days = groupItemsByDay(collection);
|
||||
}
|
||||
|
||||
let locationBeingUpdated: Location | null = null;
|
||||
@@ -142,6 +198,8 @@
|
||||
|
||||
// When opening a "create new item" modal we store the target date here
|
||||
let pendingAddDate: string | null = null;
|
||||
// Track if we've already added this location to the itinerary
|
||||
let addedToItinerary: Set<string> = new Set();
|
||||
|
||||
// Sync the locationBeingUpdated with the collection.locations array
|
||||
$: if (locationBeingUpdated && locationBeingUpdated.id && collection) {
|
||||
@@ -168,9 +226,15 @@
|
||||
|
||||
// If a new location was just created and we have a pending add-date,
|
||||
// attach it to that date in the itinerary.
|
||||
$: if (locationBeingUpdated?.id && pendingAddDate) {
|
||||
$: if (
|
||||
locationBeingUpdated?.id &&
|
||||
pendingAddDate &&
|
||||
!addedToItinerary.has(locationBeingUpdated.id)
|
||||
) {
|
||||
addItineraryItemForObject('location', locationBeingUpdated.id, pendingAddDate);
|
||||
pendingAddDate = null;
|
||||
// Mark this location as added to prevent duplicates
|
||||
addedToItinerary.add(locationBeingUpdated.id);
|
||||
addedToItinerary = addedToItinerary; // trigger reactivity
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -543,11 +607,17 @@
|
||||
|
||||
{#if isLocationModalOpen}
|
||||
<NewLocationModal
|
||||
on:close={() => (isLocationModalOpen = false)}
|
||||
on:close={() => {
|
||||
isLocationModalOpen = false;
|
||||
pendingAddDate = null;
|
||||
addedToItinerary.clear();
|
||||
addedToItinerary = addedToItinerary;
|
||||
}}
|
||||
{user}
|
||||
{locationToEdit}
|
||||
bind:location={locationBeingUpdated}
|
||||
{collection}
|
||||
initialVisitDate={pendingAddDate}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -898,7 +968,7 @@
|
||||
<LocationCard
|
||||
adventure={resolvedObj}
|
||||
on:edit={handleEditLocation}
|
||||
on:delete={handleDeleteLocation}
|
||||
on:delete={handleItemDelete}
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
{user}
|
||||
@@ -910,6 +980,7 @@
|
||||
transportation={resolvedObj}
|
||||
{user}
|
||||
{collection}
|
||||
on:delete={handleItemDelete}
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
/>
|
||||
@@ -919,6 +990,7 @@
|
||||
{user}
|
||||
{collection}
|
||||
itineraryItem={item}
|
||||
on:delete={handleItemDelete}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
/>
|
||||
{:else if objectType === 'note'}
|
||||
@@ -927,6 +999,7 @@
|
||||
note={resolvedObj}
|
||||
{user}
|
||||
{collection}
|
||||
on:delete={handleItemDelete}
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
/>
|
||||
@@ -936,6 +1009,7 @@
|
||||
checklist={resolvedObj}
|
||||
{user}
|
||||
{collection}
|
||||
on:delete={handleItemDelete}
|
||||
itineraryItem={item}
|
||||
on:removeFromItinerary={handleRemoveItineraryItem}
|
||||
/>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
export let objectId: string;
|
||||
export let trails: Trail[] = [];
|
||||
export let measurementSystem: 'metric' | 'imperial' = 'metric';
|
||||
export let initialVisitDate: string | null = null; // Used to pre-fill visit date when adding from itinerary planner
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -228,7 +229,7 @@
|
||||
}).localDate;
|
||||
}
|
||||
|
||||
async function addVisit() {
|
||||
async function addVisit(isAuto: boolean = false) {
|
||||
// If editing an existing visit, patch instead of creating new
|
||||
if (visitIdEditing) {
|
||||
const response = await fetch(`/api/visits/${visitIdEditing}/`, {
|
||||
@@ -279,12 +280,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Reset form fields
|
||||
note = '';
|
||||
localStartDate = '';
|
||||
localEndDate = '';
|
||||
utcStartDate = null;
|
||||
utcEndDate = null;
|
||||
// Reset form fields. If this call was an auto-generated visit, allow clearing even if initialVisitDate is set
|
||||
if (!initialVisitDate || isAuto) {
|
||||
note = '';
|
||||
localStartDate = '';
|
||||
localEndDate = '';
|
||||
utcStartDate = null;
|
||||
utcEndDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Activity management functions
|
||||
@@ -694,6 +697,43 @@
|
||||
} catch {
|
||||
stravaEnabled = false;
|
||||
}
|
||||
|
||||
// If initialVisitDate is provided and a visit on that date doesn't exist, create and upload a new all day visit on that date
|
||||
if (initialVisitDate) {
|
||||
const targetDate = initialVisitDate.split('T')[0]; // Ensure we have just YYYY-MM-DD
|
||||
|
||||
// Check if any visit already exists for this date
|
||||
const visitExists = visits?.some((visit) => {
|
||||
const visitStart = visit.start_date.split('T')[0];
|
||||
const visitEnd = visit.end_date.split('T')[0];
|
||||
return targetDate >= visitStart && targetDate <= visitEnd;
|
||||
});
|
||||
|
||||
if (!visitExists) {
|
||||
// Set up an all-day visit for this date
|
||||
allDay = true;
|
||||
localStartDate = targetDate;
|
||||
localEndDate = targetDate;
|
||||
|
||||
// Convert to UTC dates
|
||||
utcStartDate = updateUTCDate({
|
||||
localDate: localStartDate,
|
||||
timezone: selectedStartTimezone,
|
||||
allDay: true
|
||||
}).utcDate;
|
||||
|
||||
utcEndDate = updateUTCDate({
|
||||
localDate: localEndDate,
|
||||
timezone: selectedStartTimezone,
|
||||
allDay: true
|
||||
}).utcDate;
|
||||
|
||||
// Automatically save the visit
|
||||
await addVisit(true);
|
||||
// Clear the initialVisitDate after successful creation
|
||||
initialVisitDate = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$: isDateValid = validateDateRange(utcStartDate ?? '', utcEndDate ?? '').valid;
|
||||
@@ -852,7 +892,7 @@
|
||||
class="btn btn-{typeConfig.color} btn-sm gap-2"
|
||||
type="button"
|
||||
disabled={!localStartDate || !isDateValid}
|
||||
on:click={addVisit}
|
||||
on:click={() => addVisit(false)}
|
||||
>
|
||||
<PlusIcon class="w-4 h-4" />
|
||||
{visitIdEditing ? $t('adventures.update_visit') : $t('adventures.add_visit')}
|
||||
|
||||
Reference in New Issue
Block a user