mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-08 06:35:19 +02:00
Refactor Location and Visit types: Replace visits structure in Location with Visit type and add location, created_at, and updated_at fields to Visit
This commit is contained in:
parent
b7b7f9d26d
commit
1395efa389
12 changed files with 881 additions and 2078 deletions
|
@ -109,6 +109,9 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
|
|||
is_safe_method = request.method in permissions.SAFE_METHODS
|
||||
|
||||
# If the object has a location field, get that location and continue checking with that object, basically from the location's perspective. I am very proud of this line of code and that's why I am writing this comment.
|
||||
|
||||
print("Checking permissions for object", obj, "of type", type(obj).__name__)
|
||||
|
||||
if type(obj).__name__ == 'Trail':
|
||||
obj = obj.location
|
||||
|
||||
|
@ -117,6 +120,13 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
|
|||
if hasattr(obj, 'visit') and hasattr(obj.visit, 'location'):
|
||||
obj = obj.visit.location
|
||||
|
||||
|
||||
if type(obj).__name__ == 'Visit':
|
||||
print("Checking permissions for Visit object", obj)
|
||||
# If the object is a Visit, get its location
|
||||
if hasattr(obj, 'location'):
|
||||
obj = obj.location
|
||||
|
||||
# Anonymous users only get read access to public objects
|
||||
if not user or not user.is_authenticated:
|
||||
return is_safe_method and getattr(obj, 'is_public', False)
|
||||
|
|
|
@ -215,8 +215,13 @@ class VisitSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Visit
|
||||
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities']
|
||||
read_only_fields = ['id']
|
||||
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities','location', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
def create(self, validated_data):
|
||||
if not validated_data.get('end_date') and validated_data.get('start_date'):
|
||||
validated_data['end_date'] = validated_data['start_date']
|
||||
return super().create(validated_data)
|
||||
|
||||
class LocationSerializer(CustomModelSerializer):
|
||||
images = serializers.SerializerMethodField()
|
||||
|
@ -358,17 +363,12 @@ class LocationSerializer(CustomModelSerializer):
|
|||
return obj.is_visited_status()
|
||||
|
||||
def create(self, validated_data):
|
||||
visits_data = validated_data.pop('visits', [])
|
||||
category_data = validated_data.pop('category', None)
|
||||
collections_data = validated_data.pop('collections', [])
|
||||
|
||||
print(category_data)
|
||||
location = Location.objects.create(**validated_data)
|
||||
|
||||
# Handle visits
|
||||
for visit_data in visits_data:
|
||||
Visit.objects.create(location=location, **visit_data)
|
||||
|
||||
# Handle category
|
||||
if category_data:
|
||||
category = self.get_or_create_category(category_data)
|
||||
|
@ -384,7 +384,6 @@ class LocationSerializer(CustomModelSerializer):
|
|||
|
||||
def update(self, instance, validated_data):
|
||||
has_visits = 'visits' in validated_data
|
||||
visits_data = validated_data.pop('visits', [])
|
||||
category_data = validated_data.pop('category', None)
|
||||
|
||||
collections_data = validated_data.pop('collections', None)
|
||||
|
@ -405,27 +404,6 @@ class LocationSerializer(CustomModelSerializer):
|
|||
if collections_data is not None:
|
||||
instance.collections.set(collections_data)
|
||||
|
||||
# Handle visits
|
||||
if has_visits:
|
||||
current_visits = instance.visits.all()
|
||||
current_visit_ids = set(current_visits.values_list('id', flat=True))
|
||||
|
||||
updated_visit_ids = set()
|
||||
for visit_data in visits_data:
|
||||
visit_id = visit_data.get('id')
|
||||
if visit_id and visit_id in current_visit_ids:
|
||||
visit = current_visits.get(id=visit_id)
|
||||
for attr, value in visit_data.items():
|
||||
setattr(visit, attr, value)
|
||||
visit.save()
|
||||
updated_visit_ids.add(visit_id)
|
||||
else:
|
||||
new_visit = Visit.objects.create(location=instance, **visit_data)
|
||||
updated_visit_ids.add(new_visit.id)
|
||||
|
||||
visits_to_delete = current_visit_ids - updated_visit_ids
|
||||
instance.visits.filter(id__in=visits_to_delete).delete()
|
||||
|
||||
# call save on the location to update the updated_at field and trigger any geocoding
|
||||
instance.save()
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ router.register(r'recommendations', RecommendationsViewSet, basename='recommenda
|
|||
router.register(r'backup', BackupViewSet, basename='backup')
|
||||
router.register(r'trails', TrailViewSet, basename='trails')
|
||||
router.register(r'activities', ActivityViewSet, basename='activities')
|
||||
router.register(r'visits', VisitViewSet, basename='visits')
|
||||
|
||||
urlpatterns = [
|
||||
# Include the router under the 'api/' prefix
|
||||
|
|
|
@ -17,3 +17,4 @@ from .recommendations_view import *
|
|||
from .import_export_view import *
|
||||
from .trail_view import *
|
||||
from .activity_view import *
|
||||
from .visit_view import *
|
|
@ -3,6 +3,7 @@ from django.db.models import Q
|
|||
from adventures.models import Location, Activity
|
||||
from adventures.serializers import ActivitySerializer
|
||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
class ActivityViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ActivitySerializer
|
||||
|
@ -37,4 +38,32 @@ class ActivityViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
Set the user when creating an activity.
|
||||
"""
|
||||
visit = serializer.validated_data.get('visit')
|
||||
location = visit.location if visit else None
|
||||
|
||||
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
|
||||
raise PermissionDenied("You do not have permission to add an activity to this location.")
|
||||
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
instance = serializer.instance
|
||||
new_visit = serializer.validated_data.get('visit')
|
||||
|
||||
# Prevent changing visit/location after creation
|
||||
if new_visit and new_visit != instance.visit:
|
||||
raise PermissionDenied("Cannot change activity visit after creation. Create a new activity instead.")
|
||||
|
||||
# Check permission for updates to the existing location
|
||||
location = instance.visit.location if instance.visit else None
|
||||
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
|
||||
raise PermissionDenied("You do not have permission to update this activity.")
|
||||
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
location = instance.visit.location if instance.visit else None
|
||||
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
|
||||
raise PermissionDenied("You do not have permission to delete this activity.")
|
||||
|
||||
instance.delete()
|
|
@ -3,6 +3,7 @@ from django.db.models import Q
|
|||
from adventures.models import Location, Trail
|
||||
from adventures.serializers import TrailSerializer
|
||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
class TrailViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = TrailSerializer
|
||||
|
@ -34,7 +35,32 @@ class TrailViewSet(viewsets.ModelViewSet):
|
|||
return Trail.objects.filter(location_filter).distinct()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Set the user when creating a trail.
|
||||
"""
|
||||
location = serializer.validated_data.get('location')
|
||||
|
||||
# Optional: import this if not in the same file
|
||||
# from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
|
||||
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
|
||||
raise PermissionDenied("You do not have permission to add a trail to this location.")
|
||||
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
instance = serializer.instance
|
||||
new_location = serializer.validated_data.get('location')
|
||||
|
||||
# Prevent changing location after creation
|
||||
if new_location and new_location != instance.location:
|
||||
raise PermissionDenied("Cannot change trail location after creation. Create a new trail instead.")
|
||||
|
||||
# Check permission for updates to the existing location
|
||||
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
|
||||
raise PermissionDenied("You do not have permission to update this trail.")
|
||||
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
|
||||
raise PermissionDenied("You do not have permission to delete this trail.")
|
||||
|
||||
instance.delete()
|
66
backend/server/adventures/views/visit_view.py
Normal file
66
backend/server/adventures/views/visit_view.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from rest_framework import viewsets
|
||||
from django.db.models import Q
|
||||
from adventures.models import Location, Visit
|
||||
from adventures.serializers import VisitSerializer
|
||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
class VisitViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = VisitSerializer
|
||||
permission_classes = [IsOwnerOrSharedWithFullAccess]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Returns visits based on location permissions.
|
||||
Users can only see visits in locations they have access to for editing/updating/deleting.
|
||||
This means they are either:
|
||||
- The owner of the location
|
||||
- The location is in a collection that is shared with the user
|
||||
- The location is in a collection that the user owns
|
||||
"""
|
||||
user = self.request.user
|
||||
|
||||
if not user or not user.is_authenticated:
|
||||
return Visit.objects.none()
|
||||
|
||||
# Build the filter for accessible locations
|
||||
location_filter = Q(location__user=user) # User owns the location
|
||||
|
||||
# Location is in collections (many-to-many) that are shared with user
|
||||
location_filter |= Q(location__collections__shared_with=user)
|
||||
|
||||
# Location is in collections (many-to-many) that user owns
|
||||
location_filter |= Q(location__collections__user=user)
|
||||
|
||||
return Visit.objects.filter(location_filter).distinct()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Set the user when creating a visit and check permissions.
|
||||
"""
|
||||
location = serializer.validated_data.get('location')
|
||||
|
||||
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
|
||||
raise PermissionDenied("You do not have permission to add a visit to this location.")
|
||||
|
||||
serializer.save()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
instance = serializer.instance
|
||||
new_location = serializer.validated_data.get('location')
|
||||
|
||||
# Prevent changing location after creation
|
||||
if new_location and new_location != instance.location:
|
||||
raise PermissionDenied("Cannot change visit location after creation. Create a new visit instead.")
|
||||
|
||||
# Check permission for updates to the existing location
|
||||
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
|
||||
raise PermissionDenied("You do not have permission to update this visit.")
|
||||
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
|
||||
raise PermissionDenied("You do not have permission to delete this visit.")
|
||||
|
||||
instance.delete()
|
|
@ -39,6 +39,9 @@
|
|||
|
||||
function custom_category() {
|
||||
new_category.name = new_category.display_name.toLowerCase().replace(/ /g, '_');
|
||||
if (!new_category.icon) {
|
||||
new_category.icon = '🌎'; // Default icon if none selected
|
||||
}
|
||||
selectCategory(new_category);
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -298,7 +298,6 @@
|
|||
bind:visits={location.visits}
|
||||
bind:trails={location.trails}
|
||||
objectId={location.id}
|
||||
type="location"
|
||||
on:back={() => {
|
||||
steps[3].selected = false;
|
||||
steps[2].selected = true;
|
||||
|
|
|
@ -36,13 +36,12 @@
|
|||
|
||||
// Props
|
||||
export let collection: Collection | null = null;
|
||||
export let type: 'location' | 'transportation' | 'lodging' = 'location';
|
||||
export let selectedStartTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
export let selectedEndTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
export let utcStartDate: string | null = null;
|
||||
export let utcEndDate: string | null = null;
|
||||
export let note: string | null = null;
|
||||
export let visits: (Visit | TransportationVisit)[] | null = null;
|
||||
export let visits: Visit[] | null = null;
|
||||
export let objectId: string;
|
||||
export let trails: Trail[] = [];
|
||||
export let measurementSystem: 'metric' | 'imperial' = 'metric';
|
||||
|
@ -59,6 +58,7 @@
|
|||
let fullEndDate: string = '';
|
||||
let constrainDates: boolean = false;
|
||||
let isEditing = false;
|
||||
let visitIdEditing: string | null = null;
|
||||
|
||||
// Activity management state
|
||||
let stravaEnabled: boolean = false;
|
||||
|
@ -116,6 +116,15 @@
|
|||
'Other'
|
||||
];
|
||||
|
||||
function getTypeConfig() {
|
||||
return {
|
||||
startLabel: 'Start Date',
|
||||
endLabel: 'End Date',
|
||||
icon: CalendarIcon,
|
||||
color: 'primary'
|
||||
};
|
||||
}
|
||||
|
||||
// Reactive constraints
|
||||
$: constraintStartDate = allDay
|
||||
? fullStartDate && fullStartDate.includes('T')
|
||||
|
@ -147,7 +156,7 @@
|
|||
|
||||
const end = updateLocalDate({
|
||||
utcDate: utcEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
|
||||
timezone: selectedEndTimezone
|
||||
}).localDate;
|
||||
|
||||
localStartDate = start;
|
||||
|
@ -193,32 +202,6 @@
|
|||
return 0;
|
||||
}
|
||||
|
||||
function getTypeConfig() {
|
||||
switch (type) {
|
||||
case 'transportation':
|
||||
return {
|
||||
startLabel: 'Departure Date',
|
||||
endLabel: 'Arrival Date',
|
||||
icon: MapMarkerIcon,
|
||||
color: 'accent'
|
||||
};
|
||||
case 'lodging':
|
||||
return {
|
||||
startLabel: 'Check In',
|
||||
endLabel: 'Check Out',
|
||||
icon: CalendarIcon,
|
||||
color: 'secondary'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
startLabel: 'Start Date',
|
||||
endLabel: 'End Date',
|
||||
icon: CalendarIcon,
|
||||
color: 'primary'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
function handleLocalDateChange() {
|
||||
utcStartDate = updateUTCDate({
|
||||
|
@ -229,7 +212,7 @@
|
|||
|
||||
utcEndDate = updateUTCDate({
|
||||
localDate: localEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone,
|
||||
timezone: selectedEndTimezone,
|
||||
allDay
|
||||
}).utcDate;
|
||||
}
|
||||
|
@ -251,7 +234,7 @@
|
|||
|
||||
utcEndDate = updateUTCDate({
|
||||
localDate: localEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone,
|
||||
timezone: selectedEndTimezone,
|
||||
allDay
|
||||
}).utcDate;
|
||||
|
||||
|
@ -262,74 +245,59 @@
|
|||
|
||||
localEndDate = updateLocalDate({
|
||||
utcDate: utcEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
|
||||
timezone: selectedEndTimezone
|
||||
}).localDate;
|
||||
}
|
||||
|
||||
function createVisitObject(): Visit | TransportationVisit {
|
||||
const uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
|
||||
if (type === 'transportation') {
|
||||
const transportVisit: TransportationVisit = {
|
||||
id: uniqueId,
|
||||
start_date: utcStartDate ?? '',
|
||||
end_date: utcEndDate ?? utcStartDate ?? '',
|
||||
notes: note ?? '',
|
||||
start_timezone: selectedStartTimezone,
|
||||
end_timezone: selectedEndTimezone,
|
||||
activities: []
|
||||
};
|
||||
return transportVisit;
|
||||
} else {
|
||||
const regularVisit: Visit = {
|
||||
id: uniqueId,
|
||||
start_date: utcStartDate ?? '',
|
||||
end_date: utcEndDate ?? utcStartDate ?? '',
|
||||
notes: note ?? '',
|
||||
timezone: selectedStartTimezone,
|
||||
activities: []
|
||||
};
|
||||
return regularVisit;
|
||||
}
|
||||
}
|
||||
|
||||
async function addVisit() {
|
||||
const newVisit = createVisitObject();
|
||||
|
||||
// Patch updated visits array to location and get the response with actual IDs
|
||||
console.log('Adding new visit:', newVisit);
|
||||
console.log(objectId);
|
||||
if (type === 'location' && objectId) {
|
||||
try {
|
||||
const updatedVisits = visits ? [...visits, newVisit] : [newVisit];
|
||||
console.log('Patching visits:', updatedVisits);
|
||||
|
||||
const response = await fetch(`/api/locations/${objectId}/`, {
|
||||
// If editing an existing visit, patch instead of creating new
|
||||
if (visitIdEditing) {
|
||||
const response = await fetch(`/api/visits/${visitIdEditing}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ visits: updatedVisits })
|
||||
body: JSON.stringify({
|
||||
start_date: utcStartDate,
|
||||
end_date: utcEndDate,
|
||||
notes: note,
|
||||
timezone: selectedStartTimezone
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const updatedLocation = await response.json();
|
||||
// Update visits with the response data that contains actual IDs
|
||||
visits = updatedLocation.visits;
|
||||
const updatedVisit: Visit = await response.json();
|
||||
visits = visits ? [...visits, updatedVisit] : [updatedVisit];
|
||||
dispatch('visitAdded', updatedVisit);
|
||||
visitIdEditing = null;
|
||||
} else {
|
||||
console.error('Failed to patch visits:', await response.text());
|
||||
return; // Don't update local state if API call failed
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error patching visits:', error);
|
||||
return; // Don't update local state if API call failed
|
||||
const errorText = await response.text();
|
||||
alert(`Failed to update visit: ${errorText}`);
|
||||
}
|
||||
} else {
|
||||
// Fallback for non-location types - add new visit to the visits array
|
||||
if (visits) {
|
||||
visits = [...visits, newVisit];
|
||||
// post to /api/visits for new visit
|
||||
const response = await fetch('/api/visits/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
object_id: objectId,
|
||||
start_date: utcStartDate,
|
||||
end_date: utcEndDate,
|
||||
notes: note,
|
||||
timezone: selectedStartTimezone,
|
||||
location: objectId
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const newVisit: Visit = await response.json();
|
||||
visits = visits ? [...visits, newVisit] : [newVisit];
|
||||
dispatch('visitAdded', newVisit);
|
||||
} else {
|
||||
visits = [newVisit];
|
||||
const errorText = await response.text();
|
||||
alert(`Failed to add visit: ${errorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -581,7 +549,7 @@
|
|||
|
||||
if (response.ok) {
|
||||
// Refetch the location data to get the updated visits with correct IDs
|
||||
if (type === 'location' && objectId) {
|
||||
|
||||
const locationResponse = await fetch(`/api/locations/${objectId}/`);
|
||||
if (locationResponse.ok) {
|
||||
const updatedLocation = await locationResponse.json();
|
||||
|
@ -589,20 +557,6 @@
|
|||
} else {
|
||||
console.error('Failed to refetch location data:', await locationResponse.text());
|
||||
}
|
||||
} else {
|
||||
// Fallback: Update the visit's activities array locally
|
||||
if (visits) {
|
||||
visits = visits.map((visit) => {
|
||||
if (visit.id === visitId) {
|
||||
return {
|
||||
...visit,
|
||||
activities: (visit.activities || []).filter((a) => a.id !== activityId)
|
||||
};
|
||||
}
|
||||
return visit;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to delete activity:', await response.text());
|
||||
alert('Failed to delete activity. Please try again.');
|
||||
|
@ -661,14 +615,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
function editVisit(visit: Visit | TransportationVisit) {
|
||||
function editVisit(visit: Visit) {
|
||||
isEditing = true;
|
||||
visitIdEditing = visit.id;
|
||||
const isAllDayEvent = isAllDay(visit.start_date);
|
||||
allDay = isAllDayEvent;
|
||||
|
||||
if ('start_timezone' in visit) {
|
||||
if ('start_timezone' in visit && typeof visit.start_timezone === 'string') {
|
||||
selectedStartTimezone = visit.start_timezone;
|
||||
if ('end_timezone' in visit && typeof visit.end_timezone === 'string') {
|
||||
selectedEndTimezone = visit.end_timezone;
|
||||
}
|
||||
} else if (visit.timezone) {
|
||||
selectedStartTimezone = visit.timezone;
|
||||
}
|
||||
|
@ -684,10 +641,14 @@
|
|||
|
||||
localEndDate = updateLocalDate({
|
||||
utcDate: visit.end_date,
|
||||
timezone: 'end_timezone' in visit ? visit.end_timezone : selectedStartTimezone
|
||||
timezone:
|
||||
'end_timezone' in visit && typeof visit.end_timezone === 'string'
|
||||
? visit.end_timezone
|
||||
: selectedStartTimezone
|
||||
}).localDate;
|
||||
}
|
||||
|
||||
// Remove the visit from the array temporarily for editing
|
||||
if (visits) {
|
||||
visits = visits.filter((v) => v.id !== visit.id);
|
||||
}
|
||||
|
@ -706,17 +667,6 @@
|
|||
setTimeout(() => {
|
||||
isEditing = false;
|
||||
}, 0);
|
||||
|
||||
// Update the visits array in the parent component
|
||||
if (type === 'location' && objectId) {
|
||||
fetch(`/api/locations/${objectId}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ visits })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeVisit(visitId: string) {
|
||||
|
@ -730,16 +680,17 @@
|
|||
delete loadingActivities[visitId];
|
||||
delete showActivityUpload[visitId];
|
||||
|
||||
// Patch updated visits array to location
|
||||
if (type === 'location' && objectId) {
|
||||
fetch(`/api/locations/${objectId}/`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ visits })
|
||||
});
|
||||
// make the DELETE request
|
||||
fetch(`/api/visits/${visitId}/`, {
|
||||
method: 'DELETE'
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
alert('Failed to delete visit. Please try again.');
|
||||
} else {
|
||||
// remove the visit from the local state
|
||||
visits = visits?.filter((v) => v.id !== visitId) ?? null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
|
@ -752,10 +703,6 @@
|
|||
|
||||
// Lifecycle
|
||||
onMount(async () => {
|
||||
if ((type === 'transportation' || type === 'lodging') && utcStartDate) {
|
||||
allDay = isAllDay(utcStartDate);
|
||||
}
|
||||
|
||||
localStartDate = updateLocalDate({
|
||||
utcDate: utcStartDate,
|
||||
timezone: selectedStartTimezone
|
||||
|
@ -763,7 +710,7 @@
|
|||
|
||||
localEndDate = updateLocalDate({
|
||||
utcDate: utcEndDate,
|
||||
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
|
||||
timezone: selectedEndTimezone
|
||||
}).localDate;
|
||||
|
||||
if (!selectedStartTimezone) {
|
||||
|
@ -787,8 +734,8 @@
|
|||
}
|
||||
});
|
||||
|
||||
$: typeConfig = getTypeConfig();
|
||||
$: isDateValid = validateDateRange(utcStartDate ?? '', utcEndDate ?? '').valid;
|
||||
$: typeConfig = getTypeConfig();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gradient-to-br from-base-200/30 via-base-100 to-primary/5 p-6">
|
||||
|
@ -814,35 +761,13 @@
|
|||
|
||||
<div class="space-y-4">
|
||||
<!-- Timezone Selection -->
|
||||
{#if type === 'transportation'}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<div>
|
||||
<label class="label-text text-sm font-medium" for="departure-timezone-selector"
|
||||
>Departure Timezone</label
|
||||
>
|
||||
<label class="label-text text-sm font-medium" for="timezone-selector">Timezone</label>
|
||||
<div class="mt-1">
|
||||
<TimezoneSelector bind:selectedTimezone={selectedStartTimezone} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label-text text-sm font-medium" for="arrival-timezone-selector"
|
||||
>Arrival Timezone</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<TimezoneSelector bind:selectedTimezone={selectedEndTimezone} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<label class="label-text text-sm font-medium" for="timezone-selector"
|
||||
>Timezone</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<TimezoneSelector bind:selectedTimezone={selectedStartTimezone} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Toggles -->
|
||||
<div class="flex flex-wrap gap-6">
|
||||
|
@ -941,7 +866,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Notes (Location only) -->
|
||||
{#if type === 'location'}
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="label-text text-sm font-medium" for="visit-notes">Notes</label>
|
||||
<textarea
|
||||
|
@ -965,7 +890,6 @@
|
|||
Add Visit
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Validation Error -->
|
||||
|
@ -977,7 +901,7 @@
|
|||
{/if}
|
||||
|
||||
<!-- Visits List (Location only) -->
|
||||
{#if type === 'location'}
|
||||
|
||||
<div class="bg-base-50 p-4 rounded-lg border border-base-200">
|
||||
<h3 class="font-medium text-base-content/80 mb-4">
|
||||
Visits ({visits?.length || 0})
|
||||
|
@ -1013,9 +937,9 @@
|
|||
– {visit.end_date && typeof visit.end_date === 'string'
|
||||
? visit.end_date.split('T')[0]
|
||||
: ''}
|
||||
{:else if 'start_timezone' in visit}
|
||||
{formatDateInTimezone(visit.start_date, visit.start_timezone)}
|
||||
– {formatDateInTimezone(visit.end_date, visit.end_timezone)}
|
||||
{:else if 'start_timezone' in visit && visit.timezone}
|
||||
{formatDateInTimezone(visit.start_date, visit.timezone)}
|
||||
– {formatDateInTimezone(visit.end_date, visit.timezone)}
|
||||
{:else if visit.timezone}
|
||||
{formatDateInTimezone(visit.start_date, visit.timezone)}
|
||||
– {formatDateInTimezone(visit.end_date, visit.timezone)}
|
||||
|
@ -1113,8 +1037,7 @@
|
|||
<div class="text-sm">
|
||||
<div class="font-medium">Strava Activity Ready</div>
|
||||
<div class="text-xs opacity-75">
|
||||
GPX file downloaded. Please upload it below to complete the
|
||||
import.
|
||||
GPX file downloaded. Please upload it below to complete the import.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1124,9 +1047,7 @@
|
|||
<div class="bg-base-200/50 p-4 rounded-lg">
|
||||
{#if pendingStravaImport[visit.id]}
|
||||
<!-- Highlight GPX upload for Strava imports -->
|
||||
<div
|
||||
class="mb-6 p-4 bg-warning/10 border-2 border-warning/30 rounded-lg"
|
||||
>
|
||||
<div class="mb-6 p-4 bg-warning/10 border-2 border-warning/30 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<FileIcon class="w-4 h-4 text-warning" />
|
||||
<label
|
||||
|
@ -1215,9 +1136,8 @@
|
|||
|
||||
<!-- Distance -->
|
||||
<div>
|
||||
<label
|
||||
class="label-text text-xs font-medium"
|
||||
for="distance-{visit.id}">Distance (km)</label
|
||||
<label class="label-text text-xs font-medium" for="distance-{visit.id}"
|
||||
>Distance (km)</label
|
||||
>
|
||||
<input
|
||||
id="distance-{visit.id}"
|
||||
|
@ -1311,9 +1231,8 @@
|
|||
|
||||
<!-- Calories -->
|
||||
<div>
|
||||
<label
|
||||
class="label-text text-xs font-medium"
|
||||
for="calories-{visit.id}">Calories</label
|
||||
<label class="label-text text-xs font-medium" for="calories-{visit.id}"
|
||||
>Calories</label
|
||||
>
|
||||
<input
|
||||
id="calories-{visit.id}"
|
||||
|
@ -1359,9 +1278,8 @@
|
|||
|
||||
<!-- Rest Time -->
|
||||
<div>
|
||||
<label
|
||||
class="label-text text-xs font-medium"
|
||||
for="rest-time-{visit.id}">Rest Time (s)</label
|
||||
<label class="label-text text-xs font-medium" for="rest-time-{visit.id}"
|
||||
>Rest Time (s)</label
|
||||
>
|
||||
<input
|
||||
id="rest-time-{visit.id}"
|
||||
|
@ -1375,9 +1293,8 @@
|
|||
|
||||
<!-- Start Latitude -->
|
||||
<div>
|
||||
<label
|
||||
class="label-text text-xs font-medium"
|
||||
for="start-lat-{visit.id}">Start Latitude</label
|
||||
<label class="label-text text-xs font-medium" for="start-lat-{visit.id}"
|
||||
>Start Latitude</label
|
||||
>
|
||||
<input
|
||||
id="start-lat-{visit.id}"
|
||||
|
@ -1392,9 +1309,8 @@
|
|||
|
||||
<!-- Start Longitude -->
|
||||
<div>
|
||||
<label
|
||||
class="label-text text-xs font-medium"
|
||||
for="start-lng-{visit.id}">Start Longitude</label
|
||||
<label class="label-text text-xs font-medium" for="start-lng-{visit.id}"
|
||||
>Start Longitude</label
|
||||
>
|
||||
<input
|
||||
id="start-lng-{visit.id}"
|
||||
|
@ -1441,9 +1357,8 @@
|
|||
|
||||
<!-- Timezone -->
|
||||
<div>
|
||||
<label
|
||||
class="label-text text-xs font-medium"
|
||||
for="timezone-{visit.id}">Timezone</label
|
||||
<label class="label-text text-xs font-medium" for="timezone-{visit.id}"
|
||||
>Timezone</label
|
||||
>
|
||||
<TimezoneSelector bind:selectedTimezone={activityForm.timezone} />
|
||||
</div>
|
||||
|
@ -1467,9 +1382,8 @@
|
|||
|
||||
<!-- Max Speed -->
|
||||
<div>
|
||||
<label
|
||||
class="label-text text-xs font-medium"
|
||||
for="max-speed-{visit.id}">Max Speed (m/s)</label
|
||||
<label class="label-text text-xs font-medium" for="max-speed-{visit.id}"
|
||||
>Max Speed (m/s)</label
|
||||
>
|
||||
<input
|
||||
id="max-speed-{visit.id}"
|
||||
|
@ -1500,7 +1414,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Trail Selection -->
|
||||
{#if type === 'location' && trails && trails.length > 0}
|
||||
{#if trails && trails.length > 0}
|
||||
<div class="md:col-span-2">
|
||||
<label
|
||||
class="label-text text-xs font-medium"
|
||||
|
@ -1638,7 +1552,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -31,14 +31,7 @@ export type Location = {
|
|||
rating?: number | null;
|
||||
link?: string | null;
|
||||
images: ContentImage[];
|
||||
visits: {
|
||||
id: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
timezone: string | null;
|
||||
notes: string;
|
||||
activities: Activity[]; // Array of activities associated with the visit
|
||||
}[];
|
||||
visits: Visit[];
|
||||
collections?: string[] | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
|
@ -425,6 +418,9 @@ export type Visit = {
|
|||
notes: string;
|
||||
timezone: string | null;
|
||||
activities?: Activity[];
|
||||
location: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TransportationVisit = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue