mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Add possible places to visits
This commit is contained in:
parent
a4123791aa
commit
414c9e831c
10 changed files with 368 additions and 31 deletions
13
app/controllers/api/v1/visits/possible_places_controller.rb
Normal file
13
app/controllers/api/v1/visits/possible_places_controller.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Visits::PossiblePlacesController < ApiController
|
||||||
|
def index
|
||||||
|
visit = current_api_user.visits.find(params[:id])
|
||||||
|
# Assuming you have a method to fetch possible places
|
||||||
|
possible_places = visit.suggested_places
|
||||||
|
|
||||||
|
render json: possible_places
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render json: { error: 'Visit not found' }, status: :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -31,7 +31,7 @@ class Api::V1::VisitsController < ApiController
|
||||||
visit = current_api_user.visits.find(params[:id])
|
visit = current_api_user.visits.find(params[:id])
|
||||||
visit = update_visit(visit)
|
visit = update_visit(visit)
|
||||||
|
|
||||||
render json: visit
|
render json: Api::VisitSerializer.new(visit).call
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export default class extends BaseController {
|
||||||
trackedMonthsCache = null;
|
trackedMonthsCache = null;
|
||||||
drawerOpen = false;
|
drawerOpen = false;
|
||||||
visitCircles = L.layerGroup();
|
visitCircles = L.layerGroup();
|
||||||
|
currentPopup = null;
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
super.connect();
|
super.connect();
|
||||||
|
|
@ -103,6 +104,11 @@ export default class extends BaseController {
|
||||||
this.map.getPane('areasPane').style.zIndex = 650;
|
this.map.getPane('areasPane').style.zIndex = 650;
|
||||||
this.map.getPane('areasPane').style.pointerEvents = 'all';
|
this.map.getPane('areasPane').style.pointerEvents = 'all';
|
||||||
|
|
||||||
|
// Create custom pane for visits
|
||||||
|
this.map.createPane('visitsPane');
|
||||||
|
this.map.getPane('visitsPane').style.zIndex = 600;
|
||||||
|
this.map.getPane('visitsPane').style.pointerEvents = 'all';
|
||||||
|
|
||||||
// Initialize areasLayer as a feature group and add it to the map immediately
|
// Initialize areasLayer as a feature group and add it to the map immediately
|
||||||
this.areasLayer = new L.FeatureGroup();
|
this.areasLayer = new L.FeatureGroup();
|
||||||
this.photoMarkers = L.layerGroup();
|
this.photoMarkers = L.layerGroup();
|
||||||
|
|
@ -1382,7 +1388,7 @@ export default class extends BaseController {
|
||||||
drawerButton.innerHTML = this.drawerOpen ? '➡️' : '⬅️';
|
drawerButton.innerHTML = this.drawerOpen ? '➡️' : '⬅️';
|
||||||
}
|
}
|
||||||
|
|
||||||
const controls = document.querySelectorAll('.leaflet-control-layers, .toggle-panel-button, .leaflet-right-panel');
|
const controls = document.querySelectorAll('.leaflet-control-layers, .toggle-panel-button, .leaflet-right-panel .drawer-button');
|
||||||
controls.forEach(control => {
|
controls.forEach(control => {
|
||||||
control.classList.toggle('controls-shifted');
|
control.classList.toggle('controls-shifted');
|
||||||
});
|
});
|
||||||
|
|
@ -1402,7 +1408,7 @@ export default class extends BaseController {
|
||||||
drawer.style.maxHeight = '100vh';
|
drawer.style.maxHeight = '100vh';
|
||||||
|
|
||||||
drawer.innerHTML = `
|
drawer.innerHTML = `
|
||||||
<div class="p-4">
|
<div class="p-4 drawer">
|
||||||
<h2 class="text-xl font-bold mb-4">Recent Visits</h2>
|
<h2 class="text-xl font-bold mb-4">Recent Visits</h2>
|
||||||
<div id="visits-list" class="space-y-2">
|
<div id="visits-list" class="space-y-2">
|
||||||
<p class="text-gray-500">Loading visits...</p>
|
<p class="text-gray-500">Loading visits...</p>
|
||||||
|
|
@ -1426,6 +1432,7 @@ export default class extends BaseController {
|
||||||
const startAt = urlParams.get('start_at') || new Date().toISOString();
|
const startAt = urlParams.get('start_at') || new Date().toISOString();
|
||||||
const endAt = urlParams.get('end_at') || new Date().toISOString();
|
const endAt = urlParams.get('end_at') || new Date().toISOString();
|
||||||
|
|
||||||
|
console.log('Fetching visits for:', startAt, endAt);
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/v1/visits?start_at=${encodeURIComponent(startAt)}&end_at=${encodeURIComponent(endAt)}`,
|
`/api/v1/visits?start_at=${encodeURIComponent(startAt)}&end_at=${encodeURIComponent(endAt)}`,
|
||||||
{
|
{
|
||||||
|
|
@ -1461,22 +1468,215 @@ export default class extends BaseController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing circles
|
// Clear existing visit circles
|
||||||
this.visitCircles.clearLayers();
|
this.visitCircles.clearLayers();
|
||||||
|
|
||||||
// Draw circles only for confirmed visits
|
// Draw circles for all visits
|
||||||
visits
|
visits
|
||||||
.filter(visit => visit.status === 'confirmed')
|
.filter(visit => visit.status !== 'declined')
|
||||||
.forEach(visit => {
|
.forEach(visit => {
|
||||||
if (visit.place?.latitude && visit.place?.longitude) {
|
if (visit.place?.latitude && visit.place?.longitude) {
|
||||||
|
const isSuggested = visit.status === 'suggested';
|
||||||
const circle = L.circle([visit.place.latitude, visit.place.longitude], {
|
const circle = L.circle([visit.place.latitude, visit.place.longitude], {
|
||||||
color: '#4A90E2',
|
color: isSuggested ? '#FFA500' : '#4A90E2', // Border color
|
||||||
fillColor: '#4A90E2',
|
fillColor: isSuggested ? '#FFD700' : '#4A90E2', // Fill color
|
||||||
fillOpacity: 0.2,
|
fillOpacity: isSuggested ? 0.4 : 0.6,
|
||||||
radius: 100,
|
radius: 100,
|
||||||
weight: 2
|
weight: 2,
|
||||||
|
interactive: true,
|
||||||
|
bubblingMouseEvents: false,
|
||||||
|
pane: 'visitsPane',
|
||||||
|
dashArray: isSuggested ? '4' : null // Dotted border for suggested
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add the circle to the map
|
||||||
this.visitCircles.addLayer(circle);
|
this.visitCircles.addLayer(circle);
|
||||||
|
|
||||||
|
// Fetch possible places for the visit
|
||||||
|
const fetchPossiblePlaces = async () => {
|
||||||
|
try {
|
||||||
|
// Close any existing popup before opening a new one
|
||||||
|
if (this.currentPopup) {
|
||||||
|
this.map.closePopup(this.currentPopup);
|
||||||
|
this.currentPopup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/visits/${visit.id}/possible_places`, {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch possible places');
|
||||||
|
|
||||||
|
const possiblePlaces = await response.json();
|
||||||
|
|
||||||
|
// Create popup content with form and dropdown
|
||||||
|
const defaultName = visit.name;
|
||||||
|
const popupContent = `
|
||||||
|
<div class="p-3">
|
||||||
|
<form class="visit-name-form" data-visit-id="${visit.id}">
|
||||||
|
<div class="form-control">
|
||||||
|
<input type="text"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
value="${defaultName}"
|
||||||
|
placeholder="Enter visit name">
|
||||||
|
</div>
|
||||||
|
<div class="form-control mt-2">
|
||||||
|
<select class="select select-bordered select-sm w-full" name="place">
|
||||||
|
${possiblePlaces.map(place => `
|
||||||
|
<option value="${place.id}" ${place.id === visit.place.id ? 'selected' : ''}>
|
||||||
|
${place.name}
|
||||||
|
</option>
|
||||||
|
`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 mt-2">
|
||||||
|
<button type="submit" class="btn btn-xs btn-primary">Save</button>
|
||||||
|
${visit.status !== 'confirmed' ? `
|
||||||
|
<button type="button" class="btn btn-xs btn-success confirm-visit" data-id="${visit.id}">Confirm</button>
|
||||||
|
<button type="button" class="btn btn-xs btn-error decline-visit" data-id="${visit.id}">Decline</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create and store the popup
|
||||||
|
const popup = L.popup({
|
||||||
|
closeButton: true,
|
||||||
|
closeOnClick: false,
|
||||||
|
autoClose: false,
|
||||||
|
maxWidth: 300, // Set maximum width
|
||||||
|
minWidth: 200 // Set minimum width
|
||||||
|
})
|
||||||
|
.setLatLng([visit.place.latitude, visit.place.longitude])
|
||||||
|
.setContent(popupContent);
|
||||||
|
|
||||||
|
// Store the current popup
|
||||||
|
this.currentPopup = popup;
|
||||||
|
|
||||||
|
// Open the popup
|
||||||
|
popup.openOn(this.map);
|
||||||
|
|
||||||
|
// Add form submit handler
|
||||||
|
const form = document.querySelector(`.visit-name-form[data-visit-id="${visit.id}"]`);
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault(); // Prevent form submission
|
||||||
|
event.stopPropagation(); // Stop event bubbling
|
||||||
|
const newName = event.target.querySelector('input').value;
|
||||||
|
const selectedPlaceId = event.target.querySelector('select[name="place"]').value;
|
||||||
|
|
||||||
|
// Get the selected place name from the dropdown
|
||||||
|
const selectedOption = event.target.querySelector(`select[name="place"] option[value="${selectedPlaceId}"]`);
|
||||||
|
const selectedPlaceName = selectedOption ? selectedOption.textContent.trim() : '';
|
||||||
|
|
||||||
|
console.log('Selected new place:', selectedPlaceName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/visits/${visit.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
visit: {
|
||||||
|
name: newName,
|
||||||
|
place_id: selectedPlaceId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to update visit');
|
||||||
|
|
||||||
|
// Get the updated visit data from the response
|
||||||
|
const updatedVisit = await response.json();
|
||||||
|
|
||||||
|
// Update the local visit object with the latest data
|
||||||
|
// This ensures that if the popup is opened again, it will show the updated values
|
||||||
|
visit.name = updatedVisit.name || newName;
|
||||||
|
visit.place = updatedVisit.place;
|
||||||
|
|
||||||
|
// Use the selected place name for the update
|
||||||
|
const updatedName = selectedPlaceName || newName;
|
||||||
|
console.log('Updating visit name in drawer to:', updatedName);
|
||||||
|
|
||||||
|
// Update the visit name in the drawer panel
|
||||||
|
const drawerVisitItem = document.querySelector(`.drawer .visit-item[data-id="${visit.id}"]`);
|
||||||
|
if (drawerVisitItem) {
|
||||||
|
const nameElement = drawerVisitItem.querySelector('.font-semibold');
|
||||||
|
if (nameElement) {
|
||||||
|
console.log('Previous name in drawer:', nameElement.textContent);
|
||||||
|
nameElement.textContent = updatedName;
|
||||||
|
|
||||||
|
// Add a highlight effect to make the change visible
|
||||||
|
nameElement.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';
|
||||||
|
setTimeout(() => {
|
||||||
|
nameElement.style.backgroundColor = '';
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
console.log('Updated name in drawer to:', nameElement.textContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the popup
|
||||||
|
this.map.closePopup(popup);
|
||||||
|
this.currentPopup = null;
|
||||||
|
showFlashMessage('notice', 'Visit updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating visit:', error);
|
||||||
|
showFlashMessage('error', 'Failed to update visit');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unified handler for confirm/decline
|
||||||
|
const handleStatusChange = async (event, status) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/visits/${visit.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
visit: {
|
||||||
|
status: status
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`Failed to ${status} visit`);
|
||||||
|
|
||||||
|
this.map.closePopup(this.currentPopup);
|
||||||
|
this.currentPopup = null;
|
||||||
|
this.fetchAndDisplayVisits();
|
||||||
|
showFlashMessage('notice', `Visit ${status}d successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error ${status}ing visit:`, error);
|
||||||
|
showFlashMessage('error', `Failed to ${status} visit`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listeners for confirm and decline buttons
|
||||||
|
const confirmBtn = form.querySelector('.confirm-visit');
|
||||||
|
const declineBtn = form.querySelector('.decline-visit');
|
||||||
|
|
||||||
|
confirmBtn?.addEventListener('click', (event) => handleStatusChange(event, 'confirmed'));
|
||||||
|
declineBtn?.addEventListener('click', (event) => handleStatusChange(event, 'declined'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching possible places:', error);
|
||||||
|
showFlashMessage('error', 'Failed to load possible places');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach click event to the circle
|
||||||
|
circle.on('click', fetchPossiblePlaces);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1507,14 +1707,16 @@ export default class extends BaseController {
|
||||||
const durationText = this.formatDuration(visit.duration * 60);
|
const durationText = this.formatDuration(visit.duration * 60);
|
||||||
|
|
||||||
// Add opacity class for suggested visits
|
// Add opacity class for suggested visits
|
||||||
const bgClass = visit.status === 'suggested' ? 'bg-neutral border-dashed border-2 border-red-500' : 'bg-base-200';
|
const bgClass = visit.status === 'suggested' ? 'bg-neutral border-dashed border-2 border-sky-500' : 'bg-base-200';
|
||||||
|
const visitStyle = visit.status === 'suggested' ? 'border: 2px dashed #60a5fa;' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="p-3 rounded-lg hover:bg-base-300 transition-colors visit-item ${bgClass}"
|
<div class="w-full p-3 rounded-lg hover:bg-base-300 transition-colors visit-item ${bgClass}"
|
||||||
|
style="${visitStyle}"
|
||||||
data-lat="${visit.place?.latitude || ''}"
|
data-lat="${visit.place?.latitude || ''}"
|
||||||
data-lng="${visit.place?.longitude || ''}"
|
data-lng="${visit.place?.longitude || ''}"
|
||||||
data-id="${visit.id}">
|
data-id="${visit.id}">
|
||||||
<div class="font-semibold">${visit.name}</div>
|
<div class="font-semibold overflow-hidden">${visit.name}</div>
|
||||||
<div class="text-sm text-gray-600">
|
<div class="text-sm text-gray-600">
|
||||||
${timeDisplay.trim()}
|
${timeDisplay.trim()}
|
||||||
<span class="text-gray-500">(${durationText})</span>
|
<span class="text-gray-500">(${durationText})</span>
|
||||||
|
|
@ -1619,5 +1821,20 @@ export default class extends BaseController {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
truncateVisitNames() {
|
||||||
|
// Find all visit name elements in the drawer
|
||||||
|
const visitNameElements = document.querySelectorAll('.drawer .visit-item .font-semibold');
|
||||||
|
|
||||||
|
visitNameElements.forEach(element => {
|
||||||
|
// Add CSS classes for truncation
|
||||||
|
element.classList.add('truncate', 'max-w-[200px]', 'inline-block');
|
||||||
|
|
||||||
|
// Add tooltip with full name for hover
|
||||||
|
if (element.textContent) {
|
||||||
|
element.setAttribute('title', element.textContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
25
app/serializers/api/place_serializer.rb
Normal file
25
app/serializers/api/place_serializer.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::PlaceSerializer
|
||||||
|
def initialize(place)
|
||||||
|
@place = place
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
{
|
||||||
|
id: place.id,
|
||||||
|
name: place.name,
|
||||||
|
longitude: place.longitude,
|
||||||
|
latitude: place.latitude,
|
||||||
|
city: place.city,
|
||||||
|
country: place.country,
|
||||||
|
source: place.source,
|
||||||
|
geodata: place.geodata,
|
||||||
|
reverse_geocoded_at: place.reverse_geocoded_at
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :place
|
||||||
|
end
|
||||||
|
|
@ -17,7 +17,8 @@ class Api::VisitSerializer
|
||||||
status: status,
|
status: status,
|
||||||
place: {
|
place: {
|
||||||
latitude: visit.place&.latitude || visit.area&.latitude,
|
latitude: visit.place&.latitude || visit.area&.latitude,
|
||||||
longitude: visit.place&.longitude || visit.area&.longitude
|
longitude: visit.place&.longitude || visit.area&.longitude,
|
||||||
|
id: visit.place&.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ class ReverseGeocoding::Places::FetchData
|
||||||
limit: 10,
|
limit: 10,
|
||||||
distance_sort: true,
|
distance_sort: true,
|
||||||
radius: 1,
|
radius: 1,
|
||||||
units: ::DISTANCE_UNITS
|
units: ::DISTANCE_UNIT
|
||||||
)
|
)
|
||||||
|
|
||||||
data.reject do |place|
|
data.reject do |place|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
class Visits::SmartDetect
|
class Visits::SmartDetect
|
||||||
MINIMUM_VISIT_DURATION = 10.minutes
|
MINIMUM_VISIT_DURATION = 5.minutes
|
||||||
MAXIMUM_VISIT_GAP = 30.minutes
|
MAXIMUM_VISIT_GAP = 30.minutes
|
||||||
MINIMUM_POINTS_FOR_VISIT = 3
|
MINIMUM_POINTS_FOR_VISIT = 3
|
||||||
SIGNIFICANT_PLACE_VISITS = 2 # Number of visits to consider a place significant
|
SIGNIFICANT_PLACE_VISITS = 2 # Number of visits to consider a place significant
|
||||||
|
|
@ -321,14 +321,72 @@ class Visits::SmartDetect
|
||||||
)
|
)
|
||||||
|
|
||||||
unless place.persisted?
|
unless place.persisted?
|
||||||
place.name = visit_data[:suggested_name] || Place::DEFAULT_NAME
|
# Get reverse geocoding data
|
||||||
place.source = Place.sources[:manual]
|
geocoded_data = Geocoder.search([lat, lon])
|
||||||
|
|
||||||
|
if geocoded_data.present?
|
||||||
|
first_result = geocoded_data.first
|
||||||
|
data = first_result.data
|
||||||
|
properties = data['properties'] || {}
|
||||||
|
|
||||||
|
# Build a descriptive name from available components
|
||||||
|
name_components = [
|
||||||
|
properties['name'],
|
||||||
|
properties['street'],
|
||||||
|
properties['housenumber'],
|
||||||
|
properties['postcode'],
|
||||||
|
properties['city']
|
||||||
|
].compact.uniq
|
||||||
|
|
||||||
|
place.name = name_components.any? ? name_components.join(', ') : Place::DEFAULT_NAME
|
||||||
|
place.city = properties['city']
|
||||||
|
place.country = properties['country']
|
||||||
|
place.geodata = data
|
||||||
|
place.source = :photon
|
||||||
|
|
||||||
|
# Fetch nearby organizations
|
||||||
|
nearby_organizations = fetch_nearby_organizations(geocoded_data.drop(1))
|
||||||
|
|
||||||
|
# Save each organization as a possible place
|
||||||
|
nearby_organizations.each do |org|
|
||||||
|
Place.create!(
|
||||||
|
name: org[:name],
|
||||||
|
latitude: org[:latitude],
|
||||||
|
longitude: org[:longitude],
|
||||||
|
city: org[:city],
|
||||||
|
country: org[:country],
|
||||||
|
geodata: org[:geodata],
|
||||||
|
source: :suggested,
|
||||||
|
status: :possible
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
place.name = visit_data[:suggested_name] || Place::DEFAULT_NAME
|
||||||
|
place.source = :manual
|
||||||
|
end
|
||||||
|
|
||||||
place.save!
|
place.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
place
|
place
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_nearby_organizations(geocoded_results)
|
||||||
|
geocoded_results.map do |result|
|
||||||
|
data = result.data
|
||||||
|
properties = data['properties'] || {}
|
||||||
|
|
||||||
|
{
|
||||||
|
name: properties['name'] || 'Unknown Organization',
|
||||||
|
latitude: result.latitude,
|
||||||
|
longitude: result.longitude,
|
||||||
|
city: properties['city'],
|
||||||
|
country: properties['country'],
|
||||||
|
geodata: data
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def generate_visit_name(area, place, suggested_name)
|
def generate_visit_name(area, place, suggested_name)
|
||||||
return area.name if area
|
return area.name if area
|
||||||
return place.name if place
|
return place.name if place
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,22 @@
|
||||||
</label>
|
</label>
|
||||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
|
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
|
||||||
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
|
|
||||||
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
|
|
||||||
<li><%= link_to 'Visits & Places<sup>α</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
|
|
||||||
<li><%= link_to 'Trips<sup>α</sup>'.html_safe, trips_url, class: "#{active_class?(trips_url)}" %></li>
|
<li><%= link_to 'Trips<sup>α</sup>'.html_safe, trips_url, class: "#{active_class?(trips_url)}" %></li>
|
||||||
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
|
||||||
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
|
<li>
|
||||||
|
<details>
|
||||||
|
<summary>My data</summary>
|
||||||
|
<ul class="p-2 bg-base-100">
|
||||||
|
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
|
||||||
|
<li><%= link_to 'Visits & Places<sup>α</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
|
||||||
|
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
||||||
|
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<%= link_to 'DaWarIch', root_path, class: 'btn btn-ghost normal-case text-xl'%>
|
<%= link_to 'Dawarich', root_path, class: 'btn btn-ghost normal-case text-xl'%>
|
||||||
<div class="badge mx-4 <%= 'badge-outline' if new_version_available? %>">
|
<div class="badge mx-4 <%= 'badge-outline' if new_version_available? %>">
|
||||||
<a href="https://github.com/Freika/dawarich/releases/latest" target="_blank" class="inline-flex items-center">
|
<a href="https://github.com/Freika/dawarich/releases/latest" target="_blank" class="inline-flex items-center">
|
||||||
<% if new_version_available? %>
|
<% if new_version_available? %>
|
||||||
|
|
@ -42,12 +49,19 @@
|
||||||
<div class="navbar-center hidden lg:flex">
|
<div class="navbar-center hidden lg:flex">
|
||||||
<ul class="menu menu-horizontal px-1">
|
<ul class="menu menu-horizontal px-1">
|
||||||
<li><%= link_to 'Map', map_url, class: "mx-1 #{active_class?(map_url)}" %></li>
|
<li><%= link_to 'Map', map_url, class: "mx-1 #{active_class?(map_url)}" %></li>
|
||||||
<li><%= link_to 'Points', points_url, class: "mx-1 #{active_class?(points_url)}" %></li>
|
|
||||||
<li><%= link_to 'Stats', stats_url, class: "mx-1 #{active_class?(stats_url)}" %></li>
|
|
||||||
<li><%= link_to 'Visits & Places<sup>α</sup>'.html_safe, visits_url(status: :confirmed), class: "mx-1 #{active_class?(visits_url)}" %></li>
|
|
||||||
<li><%= link_to 'Trips<sup>α</sup>'.html_safe, trips_url, class: "mx-1 #{active_class?(trips_url)}" %></li>
|
<li><%= link_to 'Trips<sup>α</sup>'.html_safe, trips_url, class: "mx-1 #{active_class?(trips_url)}" %></li>
|
||||||
<li><%= link_to 'Imports', imports_url, class: "mx-1 #{active_class?(imports_url)}" %></li>
|
<li><%= link_to 'Stats', stats_url, class: "mx-1 #{active_class?(stats_url)}" %></li>
|
||||||
<li><%= link_to 'Exports', exports_url, class: "mx-1 #{active_class?(exports_url)}" %></li>
|
<li>
|
||||||
|
<details>
|
||||||
|
<summary>My data</summary>
|
||||||
|
<ul class="p-2 bg-base-100 rounded-box shadow-md z-10">
|
||||||
|
<li><%= link_to 'Points', points_url, class: "mx-1 #{active_class?(points_url)}" %></li>
|
||||||
|
<li><%= link_to 'Visits & Places<sup>α</sup>'.html_safe, visits_url(status: :confirmed), class: "mx-1 #{active_class?(visits_url)}" %></li>
|
||||||
|
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
||||||
|
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,10 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resources :areas, only: %i[index create update destroy]
|
resources :areas, only: %i[index create update destroy]
|
||||||
resources :points, only: %i[index create update destroy]
|
resources :points, only: %i[index create update destroy]
|
||||||
resources :visits, only: %i[index update]
|
resources :visits, only: %i[index update] do
|
||||||
resources :stats, only: :index
|
get 'possible_places', to: 'visits/possible_places#index', on: :member
|
||||||
|
end
|
||||||
|
resources :stats, only: :index
|
||||||
|
|
||||||
namespace :overland do
|
namespace :overland do
|
||||||
resources :batches, only: :create
|
resources :batches, only: :create
|
||||||
|
|
|
||||||
7
spec/requests/api/v1/visits/possible_places_spec.rb
Normal file
7
spec/requests/api/v1/visits/possible_places_spec.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "Api::V1::Visits::PossiblePlaces", type: :request do
|
||||||
|
describe "GET /index" do
|
||||||
|
pending "add some examples (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue