build out client page, edit functionality, create functionality, data massager
This commit is contained in:
parent
f510645a31
commit
34f2c110d6
15 changed files with 1571 additions and 1681 deletions
|
|
@ -1052,7 +1052,7 @@ const getBadgeColor = (status) => {
|
|||
return "success";
|
||||
case "in progress":
|
||||
case "pending":
|
||||
return "warning";
|
||||
return "warn";
|
||||
case "not started":
|
||||
case "closed":
|
||||
case "cancelled":
|
||||
|
|
|
|||
219
frontend/src/components/common/LeafletMap.vue
Normal file
219
frontend/src/components/common/LeafletMap.vue
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
<template>
|
||||
<div class="map-container">
|
||||
<div ref="mapElement" class="map" :style="{ height: mapHeight }"></div>
|
||||
<div v-if="!latitude || !longitude" class="map-overlay">
|
||||
<div class="no-coordinates">
|
||||
<i
|
||||
class="pi pi-map-marker"
|
||||
style="font-size: 2rem; color: #64748b; margin-bottom: 0.5rem"
|
||||
></i>
|
||||
<p>No coordinates available</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
// Fix Leaflet default marker icons
|
||||
delete L.Icon.Default.prototype._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
|
||||
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
|
||||
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
latitude: {
|
||||
type: [Number, String],
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
longitude: {
|
||||
type: [Number, String],
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
addressTitle: {
|
||||
type: String,
|
||||
default: "Location",
|
||||
},
|
||||
mapHeight: {
|
||||
type: String,
|
||||
default: "400px",
|
||||
},
|
||||
zoomLevel: {
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
interactive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const mapElement = ref(null);
|
||||
let map = null;
|
||||
let marker = null;
|
||||
|
||||
const initializeMap = async () => {
|
||||
if (!mapElement.value) return;
|
||||
|
||||
// Clean up existing map
|
||||
if (map) {
|
||||
map.remove();
|
||||
map = null;
|
||||
marker = null;
|
||||
}
|
||||
|
||||
const lat = parseFloat(props.latitude);
|
||||
const lng = parseFloat(props.longitude);
|
||||
|
||||
// Only create map if we have valid coordinates
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
await nextTick();
|
||||
|
||||
// Initialize map
|
||||
map = L.map(mapElement.value, {
|
||||
zoomControl: props.interactive,
|
||||
dragging: props.interactive,
|
||||
touchZoom: props.interactive,
|
||||
scrollWheelZoom: props.interactive,
|
||||
doubleClickZoom: props.interactive,
|
||||
boxZoom: props.interactive,
|
||||
keyboard: props.interactive,
|
||||
}).setView([lat, lng], props.zoomLevel);
|
||||
|
||||
// Add tile layer
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
|
||||
// Add marker
|
||||
marker = L.marker([lat, lng])
|
||||
.addTo(map)
|
||||
.bindPopup(
|
||||
`
|
||||
<div style="text-align: center;">
|
||||
<strong>${props.addressTitle}</strong><br>
|
||||
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.openPopup();
|
||||
}
|
||||
};
|
||||
|
||||
const updateMap = () => {
|
||||
const lat = parseFloat(props.latitude);
|
||||
const lng = parseFloat(props.longitude);
|
||||
|
||||
if (map && !isNaN(lat) && !isNaN(lng)) {
|
||||
// Update map view
|
||||
map.setView([lat, lng], props.zoomLevel);
|
||||
|
||||
// Update marker
|
||||
if (marker) {
|
||||
marker.setLatLng([lat, lng]);
|
||||
marker.setPopupContent(`
|
||||
<div style="text-align: center;">
|
||||
<strong>${props.addressTitle}</strong><br>
|
||||
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
marker = L.marker([lat, lng])
|
||||
.addTo(map)
|
||||
.bindPopup(
|
||||
`
|
||||
<div style="text-align: center;">
|
||||
<strong>${props.addressTitle}</strong><br>
|
||||
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.openPopup();
|
||||
}
|
||||
} else if (!isNaN(lat) && !isNaN(lng)) {
|
||||
// Coordinates available but no map yet - initialize
|
||||
initializeMap();
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for coordinate changes
|
||||
watch(
|
||||
() => [props.latitude, props.longitude, props.addressTitle],
|
||||
() => {
|
||||
updateMap();
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initializeMap();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (map) {
|
||||
map.remove();
|
||||
map = null;
|
||||
marker = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-container {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.map {
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--surface-ground);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.no-coordinates {
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.no-coordinates p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Leaflet popup customization */
|
||||
:deep(.leaflet-popup-content-wrapper) {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
:deep(.leaflet-popup-tip) {
|
||||
background: white;
|
||||
border: none;
|
||||
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue