mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 17:51:39 -05:00
Put place, area and visit info into side panel
This commit is contained in:
parent
fea34535f7
commit
f8e3c10f24
14 changed files with 238 additions and 209 deletions
File diff suppressed because one or more lines are too long
|
|
@ -13,7 +13,6 @@ export default class extends Controller {
|
|||
'longitudeInput',
|
||||
'radiusInput',
|
||||
'radiusDisplay',
|
||||
'locationDisplay',
|
||||
'submitButton',
|
||||
'submitSpinner',
|
||||
'submitText'
|
||||
|
|
@ -52,8 +51,7 @@ export default class extends Controller {
|
|||
this.latitudeInputTarget.value = center[1]
|
||||
this.longitudeInputTarget.value = center[0]
|
||||
this.radiusInputTarget.value = Math.round(radius)
|
||||
this.radiusDisplayTarget.value = Math.round(radius)
|
||||
this.locationDisplayTarget.value = `${center[1].toFixed(6)}, ${center[0].toFixed(6)}`
|
||||
this.radiusDisplayTarget.textContent = Math.round(radius)
|
||||
|
||||
// Show modal
|
||||
this.modalTarget.classList.add('modal-open')
|
||||
|
|
@ -151,8 +149,7 @@ export default class extends Controller {
|
|||
resetForm() {
|
||||
this.formTarget.reset()
|
||||
this.area = null
|
||||
this.radiusDisplayTarget.value = ''
|
||||
this.locationDisplayTarget.value = ''
|
||||
this.radiusDisplayTarget.textContent = '0'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,11 +27,30 @@ export default class extends Controller {
|
|||
const button = event.currentTarget
|
||||
const tabName = button.dataset.tab
|
||||
|
||||
this.activateTab(tabName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically switch to a tab by name
|
||||
*/
|
||||
switchToTab(tabName) {
|
||||
this.activateTab(tabName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to activate a tab
|
||||
*/
|
||||
activateTab(tabName) {
|
||||
// Find the button for this tab
|
||||
const button = this.tabButtonTargets.find(btn => btn.dataset.tab === tabName)
|
||||
|
||||
// Update active button
|
||||
this.tabButtonTargets.forEach(btn => {
|
||||
btn.classList.remove('active')
|
||||
})
|
||||
button.classList.add('active')
|
||||
if (button) {
|
||||
button.classList.add('active')
|
||||
}
|
||||
|
||||
// Update tab content
|
||||
this.tabContentTargets.forEach(content => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import maplibregl from 'maplibre-gl'
|
||||
import { PopupFactory } from 'maps_maplibre/components/popup_factory'
|
||||
import { VisitPopupFactory } from 'maps_maplibre/components/visit_popup'
|
||||
import { PhotoPopupFactory } from 'maps_maplibre/components/photo_popup'
|
||||
import { formatTimestamp } from 'maps_maplibre/utils/geojson_transformers'
|
||||
|
||||
/**
|
||||
* Handles map interaction events (clicks, popups)
|
||||
* Handles map interaction events (clicks, info display)
|
||||
*/
|
||||
export class EventHandlers {
|
||||
constructor(map) {
|
||||
constructor(map, controller) {
|
||||
this.map = map
|
||||
this.controller = controller
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -16,13 +14,18 @@ export class EventHandlers {
|
|||
*/
|
||||
handlePointClick(e) {
|
||||
const feature = e.features[0]
|
||||
const coordinates = feature.geometry.coordinates.slice()
|
||||
const properties = feature.properties
|
||||
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(PopupFactory.createPointPopup(properties))
|
||||
.addTo(this.map)
|
||||
const content = `
|
||||
<div class="space-y-2">
|
||||
<div><span class="font-semibold">Time:</span> ${formatTimestamp(properties.timestamp)}</div>
|
||||
${properties.battery ? `<div><span class="font-semibold">Battery:</span> ${properties.battery}%</div>` : ''}
|
||||
${properties.altitude ? `<div><span class="font-semibold">Altitude:</span> ${Math.round(properties.altitude)}m</div>` : ''}
|
||||
${properties.velocity ? `<div><span class="font-semibold">Speed:</span> ${Math.round(properties.velocity)} km/h</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
|
||||
this.controller.showInfo('Location Point', content)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -30,13 +33,25 @@ export class EventHandlers {
|
|||
*/
|
||||
handleVisitClick(e) {
|
||||
const feature = e.features[0]
|
||||
const coordinates = feature.geometry.coordinates.slice()
|
||||
const properties = feature.properties
|
||||
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(VisitPopupFactory.createVisitPopup(properties))
|
||||
.addTo(this.map)
|
||||
const startTime = formatTimestamp(properties.started_at)
|
||||
const endTime = formatTimestamp(properties.ended_at)
|
||||
const durationHours = Math.round(properties.duration / 3600)
|
||||
const durationDisplay = durationHours >= 1 ? `${durationHours}h` : `${Math.round(properties.duration / 60)}m`
|
||||
|
||||
const content = `
|
||||
<div class="space-y-2">
|
||||
<div class="badge badge-sm ${properties.status === 'confirmed' ? 'badge-success' : 'badge-warning'}">${properties.status}</div>
|
||||
<div><span class="font-semibold">Arrived:</span> ${startTime}</div>
|
||||
<div><span class="font-semibold">Left:</span> ${endTime}</div>
|
||||
<div><span class="font-semibold">Duration:</span> ${durationDisplay}</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const actions = [{ url: `/visits/${properties.id}`, label: 'View Details →' }]
|
||||
|
||||
this.controller.showInfo(properties.name || properties.place_name || 'Visit', content, actions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,13 +59,16 @@ export class EventHandlers {
|
|||
*/
|
||||
handlePhotoClick(e) {
|
||||
const feature = e.features[0]
|
||||
const coordinates = feature.geometry.coordinates.slice()
|
||||
const properties = feature.properties
|
||||
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(PhotoPopupFactory.createPhotoPopup(properties))
|
||||
.addTo(this.map)
|
||||
const content = `
|
||||
<div class="space-y-2">
|
||||
${properties.photo_url ? `<img src="${properties.photo_url}" alt="Photo" class="w-full rounded-lg mb-2" />` : ''}
|
||||
${properties.taken_at ? `<div><span class="font-semibold">Taken:</span> ${formatTimestamp(properties.taken_at)}</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
|
||||
this.controller.showInfo('Photo', content)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,12 +76,36 @@ export class EventHandlers {
|
|||
*/
|
||||
handlePlaceClick(e) {
|
||||
const feature = e.features[0]
|
||||
const coordinates = feature.geometry.coordinates.slice()
|
||||
const properties = feature.properties
|
||||
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(PopupFactory.createPlacePopup(properties))
|
||||
.addTo(this.map)
|
||||
const content = `
|
||||
<div class="space-y-2">
|
||||
${properties.tag ? `<div class="badge badge-sm badge-primary">${properties.tag}</div>` : ''}
|
||||
${properties.description ? `<div>${properties.description}</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
|
||||
const actions = properties.id ? [{ url: `/places/${properties.id}`, label: 'View Details →' }] : []
|
||||
|
||||
this.controller.showInfo(properties.name || 'Place', content, actions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle area click
|
||||
*/
|
||||
handleAreaClick(e) {
|
||||
const feature = e.features[0]
|
||||
const properties = feature.properties
|
||||
|
||||
const content = `
|
||||
<div class="space-y-2">
|
||||
${properties.radius ? `<div><span class="font-semibold">Radius:</span> ${Math.round(properties.radius)}m</div>` : ''}
|
||||
${properties.latitude && properties.longitude ? `<div><span class="font-semibold">Center:</span> ${properties.latitude.toFixed(6)}, ${properties.longitude.toFixed(6)}</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
|
||||
const actions = properties.id ? [{ url: `/areas/${properties.id}`, label: 'View Details →' }] : []
|
||||
|
||||
this.controller.showInfo(properties.name || 'Area', content, actions)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ export class LayerManager {
|
|||
this.map.on('click', 'visits', handlers.handleVisitClick)
|
||||
this.map.on('click', 'photos', handlers.handlePhotoClick)
|
||||
this.map.on('click', 'places', handlers.handlePlaceClick)
|
||||
// Areas have multiple layers (fill, outline, labels)
|
||||
this.map.on('click', 'areas-fill', handlers.handleAreaClick)
|
||||
this.map.on('click', 'areas-outline', handlers.handleAreaClick)
|
||||
this.map.on('click', 'areas-labels', handlers.handleAreaClick)
|
||||
|
||||
// Cursor change on hover
|
||||
this.map.on('mouseenter', 'points', () => {
|
||||
|
|
@ -88,6 +92,25 @@ export class LayerManager {
|
|||
this.map.on('mouseleave', 'places', () => {
|
||||
this.map.getCanvas().style.cursor = ''
|
||||
})
|
||||
// Areas hover handlers for all sub-layers
|
||||
this.map.on('mouseenter', 'areas-fill', () => {
|
||||
this.map.getCanvas().style.cursor = 'pointer'
|
||||
})
|
||||
this.map.on('mouseleave', 'areas-fill', () => {
|
||||
this.map.getCanvas().style.cursor = ''
|
||||
})
|
||||
this.map.on('mouseenter', 'areas-outline', () => {
|
||||
this.map.getCanvas().style.cursor = 'pointer'
|
||||
})
|
||||
this.map.on('mouseleave', 'areas-outline', () => {
|
||||
this.map.getCanvas().style.cursor = ''
|
||||
})
|
||||
this.map.on('mouseenter', 'areas-labels', () => {
|
||||
this.map.getCanvas().style.cursor = 'pointer'
|
||||
})
|
||||
this.map.on('mouseleave', 'areas-labels', () => {
|
||||
this.map.getCanvas().style.cursor = ''
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ export class MapDataManager {
|
|||
handlePointClick: this.eventHandlers.handlePointClick.bind(this.eventHandlers),
|
||||
handleVisitClick: this.eventHandlers.handleVisitClick.bind(this.eventHandlers),
|
||||
handlePhotoClick: this.eventHandlers.handlePhotoClick.bind(this.eventHandlers),
|
||||
handlePlaceClick: this.eventHandlers.handlePlaceClick.bind(this.eventHandlers)
|
||||
handlePlaceClick: this.eventHandlers.handlePlaceClick.bind(this.eventHandlers),
|
||||
handleAreaClick: this.eventHandlers.handleAreaClick.bind(this.eventHandlers)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,12 @@ export default class extends Controller {
|
|||
'selectionActions',
|
||||
'deleteButtonText',
|
||||
'selectedVisitsContainer',
|
||||
'selectedVisitsBulkActions'
|
||||
'selectedVisitsBulkActions',
|
||||
// Info display
|
||||
'infoDisplay',
|
||||
'infoTitle',
|
||||
'infoContent',
|
||||
'infoActions'
|
||||
]
|
||||
|
||||
async connect() {
|
||||
|
|
@ -88,7 +93,7 @@ export default class extends Controller {
|
|||
// Initialize managers
|
||||
this.layerManager = new LayerManager(this.map, this.settings, this.api)
|
||||
this.dataLoader = new DataLoader(this.api, this.apiKeyValue)
|
||||
this.eventHandlers = new EventHandlers(this.map)
|
||||
this.eventHandlers = new EventHandlers(this.map, this)
|
||||
this.filterManager = new FilterManager(this.dataLoader)
|
||||
this.mapDataManager = new MapDataManager(this)
|
||||
|
||||
|
|
@ -335,4 +340,50 @@ export default class extends Controller {
|
|||
toggleSpeedColoredRoutes(event) { return this.routesManager.toggleSpeedColoredRoutes(event) }
|
||||
openSpeedColorEditor() { return this.routesManager.openSpeedColorEditor() }
|
||||
handleSpeedColorSave(event) { return this.routesManager.handleSpeedColorSave(event) }
|
||||
|
||||
// Info Display methods
|
||||
showInfo(title, content, actions = []) {
|
||||
if (!this.hasInfoDisplayTarget) return
|
||||
|
||||
// Set title
|
||||
this.infoTitleTarget.textContent = title
|
||||
|
||||
// Set content
|
||||
this.infoContentTarget.innerHTML = content
|
||||
|
||||
// Set actions
|
||||
if (actions.length > 0) {
|
||||
this.infoActionsTarget.innerHTML = actions.map(action =>
|
||||
`<a href="${action.url}" class="btn btn-sm btn-primary">${action.label}</a>`
|
||||
).join('')
|
||||
} else {
|
||||
this.infoActionsTarget.innerHTML = ''
|
||||
}
|
||||
|
||||
// Show info display
|
||||
this.infoDisplayTarget.classList.remove('hidden')
|
||||
|
||||
// Switch to tools tab and open panel
|
||||
this.switchToToolsTab()
|
||||
}
|
||||
|
||||
closeInfo() {
|
||||
if (!this.hasInfoDisplayTarget) return
|
||||
this.infoDisplayTarget.classList.add('hidden')
|
||||
}
|
||||
|
||||
switchToToolsTab() {
|
||||
// Open the panel if it's not already open
|
||||
if (!this.settingsPanelTarget.classList.contains('open')) {
|
||||
this.toggleSettings()
|
||||
}
|
||||
|
||||
// Find the map-panel controller and switch to tools tab
|
||||
const panelElement = this.settingsPanelTarget
|
||||
const panelController = this.application.getControllerForElementAndIdentifier(panelElement, 'map-panel')
|
||||
|
||||
if (panelController && panelController.switchToTab) {
|
||||
panelController.switchToTab('tools')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,7 @@ export default class extends Controller {
|
|||
'startTimeInput',
|
||||
'endTimeInput',
|
||||
'latitudeInput',
|
||||
'longitudeInput',
|
||||
'locationDisplay',
|
||||
'submitButton',
|
||||
'submitSpinner',
|
||||
'submitText'
|
||||
'longitudeInput'
|
||||
]
|
||||
|
||||
static values = {
|
||||
|
|
@ -28,7 +24,6 @@ export default class extends Controller {
|
|||
console.log('[Visit Creation V2] Controller connected')
|
||||
this.marker = null
|
||||
this.mapController = null
|
||||
this.adjustingLocation = false
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
|
@ -52,9 +47,6 @@ export default class extends Controller {
|
|||
this.startTimeInputTarget.value = this.formatDateTime(now)
|
||||
this.endTimeInputTarget.value = this.formatDateTime(oneHourLater)
|
||||
|
||||
// Update location display
|
||||
this.updateLocationDisplay()
|
||||
|
||||
// Show modal
|
||||
this.modalTarget.classList.add('modal-open')
|
||||
|
||||
|
|
@ -79,17 +71,6 @@ export default class extends Controller {
|
|||
|
||||
// Remove marker
|
||||
this.removeMarker()
|
||||
|
||||
// Exit adjust location mode if active
|
||||
if (this.adjustingLocation) {
|
||||
this.exitAdjustLocationMode()
|
||||
}
|
||||
|
||||
// Clean up map click listener
|
||||
if (this.mapController && this.mapClickHandler) {
|
||||
this.mapController.map.off('click', this.mapClickHandler)
|
||||
this.mapClickHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,11 +81,6 @@ export default class extends Controller {
|
|||
|
||||
console.log('[Visit Creation V2] Submitting form')
|
||||
|
||||
// Disable submit button and show spinner
|
||||
this.submitButtonTarget.disabled = true
|
||||
this.submitSpinnerTarget.classList.remove('hidden')
|
||||
this.submitTextTarget.textContent = 'Creating...'
|
||||
|
||||
const formData = new FormData(this.formTarget)
|
||||
|
||||
const visitData = {
|
||||
|
|
@ -151,82 +127,9 @@ export default class extends Controller {
|
|||
} catch (error) {
|
||||
console.error('[Visit Creation V2] Error creating visit:', error)
|
||||
this.showToast(error.message || 'Failed to create visit', 'error')
|
||||
|
||||
// Re-enable submit button
|
||||
this.submitButtonTarget.disabled = false
|
||||
this.submitSpinnerTarget.classList.add('hidden')
|
||||
this.submitTextTarget.textContent = 'Create Visit'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter adjust location mode
|
||||
*/
|
||||
adjustLocation() {
|
||||
console.log('[Visit Creation V2] Entering adjust location mode')
|
||||
|
||||
if (!this.mapController) return
|
||||
|
||||
this.adjustingLocation = true
|
||||
|
||||
// Change cursor to crosshair
|
||||
this.mapController.map.getCanvas().style.cursor = 'crosshair'
|
||||
|
||||
// Show info message
|
||||
this.showToast('Click on the map to adjust visit location', 'info')
|
||||
|
||||
// Add map click listener
|
||||
this.mapClickHandler = (e) => {
|
||||
const { lng, lat } = e.lngLat
|
||||
this.updateLocation(lat, lng)
|
||||
}
|
||||
|
||||
this.mapController.map.once('click', this.mapClickHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit adjust location mode
|
||||
*/
|
||||
exitAdjustLocationMode() {
|
||||
if (!this.mapController) return
|
||||
|
||||
this.adjustingLocation = false
|
||||
this.mapController.map.getCanvas().style.cursor = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Update location coordinates
|
||||
*/
|
||||
updateLocation(lat, lng) {
|
||||
console.log('[Visit Creation V2] Updating location', { lat, lng })
|
||||
|
||||
this.latitudeInputTarget.value = lat
|
||||
this.longitudeInputTarget.value = lng
|
||||
|
||||
// Update location display
|
||||
this.updateLocationDisplay()
|
||||
|
||||
// Update marker position
|
||||
if (this.marker) {
|
||||
this.marker.setLngLat([lng, lat])
|
||||
} else {
|
||||
this.addMarker(lat, lng)
|
||||
}
|
||||
|
||||
// Exit adjust location mode
|
||||
this.exitAdjustLocationMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update location display text
|
||||
*/
|
||||
updateLocationDisplay() {
|
||||
const lat = parseFloat(this.latitudeInputTarget.value)
|
||||
const lng = parseFloat(this.longitudeInputTarget.value)
|
||||
|
||||
this.locationDisplayTarget.value = `${lat.toFixed(6)}, ${lng.toFixed(6)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Add marker to map
|
||||
*/
|
||||
|
|
@ -241,20 +144,13 @@ export default class extends Controller {
|
|||
el.className = 'visit-creation-marker'
|
||||
el.innerHTML = '📍'
|
||||
el.style.fontSize = '30px'
|
||||
el.style.cursor = 'pointer'
|
||||
|
||||
// Use maplibregl if available (from mapController)
|
||||
const maplibregl = window.maplibregl
|
||||
if (maplibregl) {
|
||||
this.marker = new maplibregl.Marker({ element: el, draggable: true })
|
||||
this.marker = new maplibregl.Marker({ element: el })
|
||||
.setLngLat([lng, lat])
|
||||
.addTo(this.mapController.map)
|
||||
|
||||
// Update coordinates on drag
|
||||
this.marker.on('dragend', () => {
|
||||
const lngLat = this.marker.getLngLat()
|
||||
this.updateLocation(lngLat.lat, lngLat.lng)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,11 +169,6 @@ export default class extends Controller {
|
|||
*/
|
||||
cleanup() {
|
||||
this.removeMarker()
|
||||
|
||||
if (this.mapController && this.mapClickHandler) {
|
||||
this.mapController.map.off('click', this.mapClickHandler)
|
||||
this.mapClickHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -50,25 +50,33 @@ export class VisitPopupFactory {
|
|||
<style>
|
||||
.visit-popup {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
min-width: 250px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid ${colors.border};
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.popup-header strong {
|
||||
font-size: 15px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.visit-badge {
|
||||
padding: 2px 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.visit-badge.suggested {
|
||||
|
|
@ -83,39 +91,40 @@ export class VisitPopupFactory {
|
|||
|
||||
.popup-body {
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 4px 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.popup-row .label {
|
||||
color: ${colors.textMuted};
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.popup-row .value {
|
||||
font-weight: 500;
|
||||
color: ${colors.textPrimary};
|
||||
display: block;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
padding-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid ${colors.border};
|
||||
}
|
||||
|
||||
.view-details-btn {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 6px 12px;
|
||||
padding: 10px 16px;
|
||||
background: ${colors.accent};
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,23 @@ export function pointsToGeoJSON(points) {
|
|||
|
||||
/**
|
||||
* Format timestamp for display
|
||||
* @param {number} timestamp - Unix timestamp
|
||||
* @param {number|string} timestamp - Unix timestamp (seconds) or ISO 8601 string
|
||||
* @returns {string} Formatted date/time
|
||||
*/
|
||||
export function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp * 1000)
|
||||
// Handle different timestamp formats
|
||||
let date
|
||||
if (typeof timestamp === 'string') {
|
||||
// ISO 8601 string
|
||||
date = new Date(timestamp)
|
||||
} else if (timestamp < 10000000000) {
|
||||
// Unix timestamp in seconds
|
||||
date = new Date(timestamp * 1000)
|
||||
} else {
|
||||
// Unix timestamp in milliseconds
|
||||
date = new Date(timestamp)
|
||||
}
|
||||
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
|
|
|
|||
|
|
@ -29,31 +29,14 @@
|
|||
<label class="label">
|
||||
<span class="label-text font-semibold">Radius</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
data-area-creation-v2-target="radiusDisplay"
|
||||
readonly>
|
||||
<div class="badge badge-info">meters</div>
|
||||
<div class="text-lg font-semibold">
|
||||
<span data-area-creation-v2-target="radiusDisplay">0</span> meters
|
||||
</div>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Draw on the map to set the radius</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Center Location -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Center Location</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full text-sm"
|
||||
data-area-creation-v2-target="locationDisplay"
|
||||
readonly>
|
||||
</div>
|
||||
|
||||
<!-- Drawing Instructions -->
|
||||
<div class="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
|
|
@ -73,7 +56,7 @@
|
|||
<div class="modal-action">
|
||||
<button type="button" class="btn btn-ghost" data-action="click->area-creation-v2#close">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" data-area-creation-v2-target="submitButton">
|
||||
<span class="loading loading-spinner loading-sm hidden" data-area-creation-v2-target="submitSpinner"></span>
|
||||
<span class="loading loading-sm hidden" data-area-creation-v2-target="submitSpinner"></span>
|
||||
<span data-area-creation-v2-target="submitText">Create Area</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
<button class="btn btn-ghost btn-sm btn-circle"
|
||||
data-action="click->maps--maplibre#toggleSettings"
|
||||
title="Close panel">
|
||||
<%= icon 'search' %>
|
||||
<%= icon 'x' %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -563,6 +563,24 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Info Display (shown when clicking on visit/area/place) -->
|
||||
<div class="hidden mt-4" data-maps--maplibre-target="infoDisplay">
|
||||
<div class="card bg-base-200 shadow-md">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h4 class="card-title text-base" data-maps--maplibre-target="infoTitle"></h4>
|
||||
<button class="btn btn-ghost btn-xs btn-circle" data-action="click->maps--maplibre#closeInfo" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="space-y-2 text-sm" data-maps--maplibre-target="infoContent">
|
||||
<!-- Content will be dynamically inserted -->
|
||||
</div>
|
||||
<div class="card-actions justify-end mt-3" data-maps--maplibre-target="infoActions">
|
||||
<!-- Action buttons will be dynamically inserted -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selection Actions (shown after area is selected) -->
|
||||
<div class="hidden mt-4 space-y-2" data-maps--maplibre-target="selectionActions">
|
||||
<button type="button"
|
||||
|
|
|
|||
|
|
@ -47,36 +47,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Location</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
data-visit-creation-v2-target="locationDisplay"
|
||||
readonly>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
data-action="click->visit-creation-v2#adjustLocation"
|
||||
title="Adjust location on map">
|
||||
📍 Adjust
|
||||
</button>
|
||||
</div>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Click on the map to change location</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn btn-ghost" data-action="click->visit-creation-v2#close">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" data-visit-creation-v2-target="submitButton">
|
||||
<span class="loading loading-spinner loading-sm hidden" data-visit-creation-v2-target="submitSpinner"></span>
|
||||
<span data-visit-creation-v2-target="submitText">Create Visit</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">Create Visit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -167,7 +167,15 @@ test.describe('Routes Layer', () => {
|
|||
|
||||
test.describe('Persistence', () => {
|
||||
test('date navigation preserves routes layer', async ({ page }) => {
|
||||
await page.waitForTimeout(1000)
|
||||
// Wait for routes layer to be added to the map
|
||||
await page.waitForFunction(() => {
|
||||
const element = document.querySelector('[data-controller*="maps--maplibre"]')
|
||||
if (!element) return false
|
||||
const app = window.Stimulus || window.Application
|
||||
if (!app) return false
|
||||
const controller = app.getControllerForElementAndIdentifier(element, 'maps--maplibre')
|
||||
return controller?.map?.getLayer('routes') !== undefined
|
||||
}, { timeout: 10000 })
|
||||
|
||||
const initialRoutes = await hasLayer(page, 'routes')
|
||||
expect(initialRoutes).toBe(true)
|
||||
|
|
|
|||
Loading…
Reference in a new issue