Get markers to show up on maps in pool review page
This commit is contained in:
parent
5549f9d79f
commit
05ec6798ac
7 changed files with 416 additions and 116 deletions
|
|
@ -1,12 +1,13 @@
|
|||
<template>
|
||||
<p>A flyover pool</p>
|
||||
<div v-if="session.organization">
|
||||
<div v-if="session.organization && session.urls">
|
||||
<MapProxiedArcgisTile
|
||||
:location="location"
|
||||
:markers="markers"
|
||||
:organizationId="session.organization.id"
|
||||
:tegola="session.urls?.tegola ?? ''"
|
||||
:urlTiles="session.urls?.tile ?? ''"
|
||||
:tegola="session.urls!.tegola"
|
||||
:urlTiles="session.urls!.tile"
|
||||
v-model="cameraFlyover"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
@ -15,15 +16,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import MapProxiedArcgisTile from "@/components/MapProxiedArcgisTile.vue";
|
||||
import { Location } from "@/type/api";
|
||||
import { Marker } from "@/types";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
import { Marker } from "@/types";
|
||||
import { Location } from "@/type/api";
|
||||
import { Camera } from "@/type/map";
|
||||
|
||||
interface Props {
|
||||
location: Location;
|
||||
markers: Marker[];
|
||||
}
|
||||
const cameraFlyover = ref<Camera>(new Camera());
|
||||
const props = defineProps<Props>();
|
||||
const session = useSessionStore();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,92 +1,271 @@
|
|||
<style scoped>
|
||||
#map {
|
||||
height: 100%;
|
||||
<style scoped lang="scss">
|
||||
@import url("https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.css");
|
||||
.map-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.map-container {
|
||||
height: 100%;
|
||||
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
.map-inactive {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.map-overlay:hover {
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
text-align: center;
|
||||
color: #0d6efd;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.overlay-content i {
|
||||
display: block;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.map-status-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 2;
|
||||
border: none;
|
||||
color: #000;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
font-size: 0.875rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.map-status-btn.locked {
|
||||
background: $warning;
|
||||
}
|
||||
.map-status-btn.unlocked {
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
.map-status-btn:hover {
|
||||
background: #ffb300;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.map-status-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
.overlay-content {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.overlay-content i {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.map-status-btn {
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure map fills container on all devices */
|
||||
:deep(.maplibregl-map) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.maplibregl-canvas) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div v-if="error == null">
|
||||
<div ref="mapContainer" class="map-multipoint"></div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1>Map failed to load</h1>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
<div ref="mapContainer" class="map-container"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import type { LngLatBoundsLike, Map as MapLibreMap } from "maplibre-gl";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
shallowRef,
|
||||
ref,
|
||||
type Ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from "vue";
|
||||
|
||||
import { boundsMarkers, boundsDefault } from "@/map-utils";
|
||||
import { MapClickEvent, Marker, Point } from "@/types";
|
||||
import type { Location } from "@/type/api";
|
||||
import type { Camera, MoveEndEventInternal } from "@/type/map";
|
||||
|
||||
interface Emits {
|
||||
(e: "map-click", event: MapClickEvent): void;
|
||||
(e: "update:modelValue", value: Camera): void;
|
||||
}
|
||||
interface Props {
|
||||
location: Location;
|
||||
markers: Marker[];
|
||||
initialCamera?: Camera;
|
||||
modelValue: Camera;
|
||||
markers?: Marker[];
|
||||
organizationId: Number;
|
||||
tegola: string;
|
||||
urlTiles: string;
|
||||
}
|
||||
const emit = defineEmits<Emits>();
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
markers: () => [],
|
||||
});
|
||||
|
||||
const error = ref<string | null>(null);
|
||||
const isLoaded = ref<boolean>(false);
|
||||
const map: Ref<maplibregl.Map | null> = shallowRef(null);
|
||||
const mapContainer = ref<HTMLElement | null>(null);
|
||||
const map: Ref<MapLibreMap | null> = shallowRef(null);
|
||||
const markerInstances = ref<Map<string, maplibregl.Marker>>(new Map());
|
||||
const markers = ref<Map<string, maplibregl.Marker>>(new Map());
|
||||
const mapMarkers: Ref<Map<string, maplibregl.Marker>> = shallowRef<
|
||||
Map<string, maplibregl.Marker>
|
||||
>(new Map());
|
||||
|
||||
// Watch for latitude/longitude changes
|
||||
watch(
|
||||
() => [props.location],
|
||||
([newLocation]) => {
|
||||
// Frame all markers in view
|
||||
function frameMarkers() {
|
||||
if (!map.value || props.markers.length === 0) return;
|
||||
|
||||
if (props.markers.length === 1) {
|
||||
// Single marker: pan to it
|
||||
// If we are zoomed way out we are likely in the default state antd therefore should zoom in a bunch
|
||||
// for the framing.
|
||||
const zoom = props.modelValue.zoom > 1 ? props.modelValue.zoom : 15;
|
||||
console.log(
|
||||
"framing single marker",
|
||||
isLoaded.value,
|
||||
props.markers[0].location,
|
||||
props.modelValue.zoom,
|
||||
zoom,
|
||||
);
|
||||
|
||||
// Defer this until the map is loaded or we'll drop updates
|
||||
if (map.value) {
|
||||
map.value.jumpTo({
|
||||
center: [newLocation.longitude, newLocation.latitude],
|
||||
zoom: 19,
|
||||
});
|
||||
if (isLoaded.value) {
|
||||
panToLocation(props.markers[0].location, zoom);
|
||||
} else {
|
||||
map.value.on("load", () => {
|
||||
panToLocation(props.markers[0].location, zoom);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error("Can't frame markers before the map is created");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
} else {
|
||||
// Multiple markers: fit bounds
|
||||
console.log("framing multiple markers", props.markers);
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
props.markers.forEach((marker) => {
|
||||
bounds.extend([marker.location.longitude, marker.location.latitude]);
|
||||
});
|
||||
map.value.fitBounds(
|
||||
bounds,
|
||||
{ padding: 10, duration: 1000 },
|
||||
{ isInternalUpdate: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
const initializeMap = () => {
|
||||
if (!mapContainer.value) return;
|
||||
|
||||
try {
|
||||
map.value = new maplibregl.Map({
|
||||
center: [props.location.longitude, props.location.latitude],
|
||||
const _map = new maplibregl.Map({
|
||||
container: mapContainer.value,
|
||||
style: "https://tiles.stadiamaps.com/styles/osm_bright.json",
|
||||
zoom: 19,
|
||||
});
|
||||
const mapInstance = map.value;
|
||||
|
||||
mapInstance.on("load", () => {
|
||||
if (props.markers.length > 0) {
|
||||
console.log("initial map fitting initial markers", props.markers);
|
||||
_map.fitBounds(boundsMarkers(props.markers));
|
||||
} else if (
|
||||
props.initialCamera &&
|
||||
(props.initialCamera.location.latitude ||
|
||||
props.initialCamera.location.longitude)
|
||||
) {
|
||||
console.log("initial map jump to initial camera", props.initialCamera);
|
||||
_map.jumpTo({
|
||||
center: [
|
||||
props.initialCamera.location.longitude,
|
||||
props.initialCamera.location.latitude,
|
||||
],
|
||||
zoom: props.initialCamera.zoom,
|
||||
});
|
||||
} else if (
|
||||
props.modelValue.location.latitude != 0 ||
|
||||
props.modelValue.location.longitude != 0
|
||||
) {
|
||||
console.log("initial map jump to initial model", props.modelValue);
|
||||
_map.jumpTo({
|
||||
center: [
|
||||
props.modelValue.location.longitude,
|
||||
props.modelValue.location.latitude,
|
||||
],
|
||||
zoom: props.modelValue.zoom,
|
||||
});
|
||||
} else {
|
||||
const bounds = boundsDefault();
|
||||
console.log("initial map fitting default bounds", bounds);
|
||||
_map.fitBounds(bounds);
|
||||
}
|
||||
_map.addControl(new maplibregl.NavigationControl(), "top-left");
|
||||
map.value = _map;
|
||||
_map.on("load", () => {
|
||||
isLoaded.value = true;
|
||||
if (props.organizationId !== 0) {
|
||||
mapInstance.addSource("tegola", {
|
||||
_map.addSource("tegola", {
|
||||
type: "vector",
|
||||
tiles: [
|
||||
`${props.tegola}maps/nidus/{z}/{x}/{y}?id=${props.organizationId}&organization_id=${props.organizationId}`,
|
||||
],
|
||||
});
|
||||
mapInstance.addLayer({
|
||||
_map.addLayer({
|
||||
id: "service-area",
|
||||
source: "tegola",
|
||||
"source-layer": "service-area-bounds",
|
||||
|
|
@ -97,32 +276,109 @@ const initializeMap = () => {
|
|||
});
|
||||
}
|
||||
|
||||
mapInstance.addSource("flyover", {
|
||||
_map.addSource("flyover", {
|
||||
type: "raster",
|
||||
tiles: [props.urlTiles],
|
||||
});
|
||||
|
||||
mapInstance.addLayer({
|
||||
_map.addLayer({
|
||||
id: "flyover-layer",
|
||||
source: "flyover",
|
||||
type: "raster",
|
||||
});
|
||||
|
||||
mapInstance.on("click", (e) => {
|
||||
_map.on("click", (e) => {
|
||||
emit("map-click", {
|
||||
location: {
|
||||
latitude: e.lngLat.lat,
|
||||
longitude: e.lngLat.lng,
|
||||
},
|
||||
map: mapInstance,
|
||||
map: _map,
|
||||
point: e.point,
|
||||
});
|
||||
});
|
||||
console.log("MapProxiedArcgisTile loaded");
|
||||
});
|
||||
console.log("MapProxiedArcgisTile initialized");
|
||||
} catch (e) {
|
||||
console.error("hey dummy", e);
|
||||
}
|
||||
};
|
||||
function panToLocation(location: Location, zoom: number) {
|
||||
if (!map.value) return;
|
||||
map.value.panTo(
|
||||
{
|
||||
lat: props.markers[0].location.latitude,
|
||||
lng: props.markers[0].location.longitude,
|
||||
},
|
||||
{ duration: 1000, zoom: zoom },
|
||||
{ isInternalUpdate: true },
|
||||
);
|
||||
}
|
||||
|
||||
// Update markers on the map
|
||||
const updateMarkers = () => {
|
||||
if (!map.value) return;
|
||||
|
||||
// Remove markers that no longer exist
|
||||
const currentMarkerIds = new Set(props.markers.map((m) => m.id));
|
||||
for (const [id, marker] of mapMarkers.value) {
|
||||
if (!currentMarkerIds.has(id)) {
|
||||
marker.remove();
|
||||
mapMarkers.value.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update markers
|
||||
props.markers.forEach((markerData) => {
|
||||
let marker = mapMarkers.value.get(markerData.id);
|
||||
|
||||
if (marker) {
|
||||
// Update existing marker
|
||||
marker.setLngLat([
|
||||
markerData.location.longitude,
|
||||
markerData.location.latitude,
|
||||
]);
|
||||
marker.setDraggable(markerData.draggable ?? false);
|
||||
} else {
|
||||
// Create new marker
|
||||
const el = document.createElement("div");
|
||||
el.className = "custom-marker";
|
||||
el.style.backgroundColor = markerData.color ?? "#FF0000";
|
||||
el.style.width = "25px";
|
||||
el.style.height = "25px";
|
||||
el.style.borderRadius = "50%";
|
||||
el.style.border = "3px solid white";
|
||||
el.style.boxShadow = "0 2px 4px rgba(0,0,0,0.3)";
|
||||
el.style.cursor = markerData.draggable ? "move" : "pointer";
|
||||
|
||||
marker = new maplibregl.Marker({
|
||||
element: el,
|
||||
draggable: markerData.draggable ?? false,
|
||||
})
|
||||
.setLngLat([
|
||||
markerData.location.longitude,
|
||||
markerData.location.latitude,
|
||||
])
|
||||
.addTo(map.value!);
|
||||
|
||||
// Handle marker drag end
|
||||
if (markerData.draggable) {
|
||||
marker.on("dragend", () => {
|
||||
const lngLat = marker!.getLngLat();
|
||||
const location: Location = {
|
||||
latitude: lngLat.lat,
|
||||
longitude: lngLat.lng,
|
||||
};
|
||||
//emit("marker-drag-end", location);
|
||||
});
|
||||
}
|
||||
|
||||
mapMarkers.value.set(markerData.id, marker);
|
||||
}
|
||||
});
|
||||
frameMarkers();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => initializeMap(), 0);
|
||||
|
|
@ -133,4 +389,30 @@ onBeforeUnmount(() => {
|
|||
map.value.remove();
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newCamera) => {
|
||||
if (map.value && newCamera) {
|
||||
console.log("panning based on model change", newCamera);
|
||||
map.value.panTo(
|
||||
{
|
||||
lat: newCamera.location.latitude,
|
||||
lng: newCamera.location.longitude,
|
||||
},
|
||||
{ duration: 1000, zoom: newCamera.zoom },
|
||||
{ isInternalUpdate: true },
|
||||
);
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Watch for markers changes
|
||||
watch(
|
||||
() => props.markers,
|
||||
() => {
|
||||
updateMarkers();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -52,21 +52,24 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showMapTile" class="map-container">
|
||||
<MapProxiedArcgisTile
|
||||
:location="selectedSignalLocation()"
|
||||
:markers="[]"
|
||||
:organizationId="session.organization?.id ?? 0"
|
||||
:tegola="session.urls?.tegola ?? ''"
|
||||
:urlTiles="session.urls?.tile ?? ''"
|
||||
@map-click="updateSignalLocation"
|
||||
>
|
||||
</MapProxiedArcgisTile>
|
||||
</div>
|
||||
<template v-if="session.organization && session.urls">
|
||||
<div v-show="showMapTile" class="map-container">
|
||||
<MapProxiedArcgisTile
|
||||
@map-click="updateSignalLocation"
|
||||
:markers="[]"
|
||||
:organizationId="session.organization!.id"
|
||||
:tegola="session.urls!.tegola"
|
||||
:urlTiles="session.urls!.tile"
|
||||
v-model="mapFlyoverCamera"
|
||||
>
|
||||
</MapProxiedArcgisTile>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import MapMultipoint from "@/components/MapMultipoint.vue";
|
||||
import MapProxiedArcgisTile from "@/components/MapProxiedArcgisTile.vue";
|
||||
import PlanningColumnDetailEntry from "@/components/PlanningColumnDetailEntry.vue";
|
||||
|
|
@ -75,11 +78,13 @@ import { shortAddress } from "@/format";
|
|||
import { useSessionStore } from "@/store/session";
|
||||
import { MapClickEvent, Marker } from "@/types";
|
||||
import type { Location, Signal } from "@/type/api";
|
||||
import { Camera } from "@/type/map";
|
||||
|
||||
interface Props {
|
||||
markers: Marker[];
|
||||
selectedSignals: Array<Signal>;
|
||||
}
|
||||
const mapFlyoverCamera = ref<Camera>(new Camera());
|
||||
const props = defineProps<Props>();
|
||||
const session = useSessionStore();
|
||||
const configureMapTile = () => {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@
|
|||
/* Prevent touch scrolling issues */
|
||||
touch-action: pan-y pinch-zoom;
|
||||
}
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<!-- No Selection State -->
|
||||
|
|
@ -135,26 +131,29 @@
|
|||
|
||||
<!-- Map Components -->
|
||||
<div class="map-container" v-if="session.organization">
|
||||
<MapLocator :markers="markers" v-model="mapCamera"></MapLocator>
|
||||
<MapLocator :markers="mapMarkers" v-model="mapCamera"></MapLocator>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>loading...</p>
|
||||
</div>
|
||||
|
||||
<div class="map-container" v-if="session.organization && selectedTask.pool">
|
||||
<div
|
||||
class="map-container"
|
||||
v-if="session.organization && selectedTask.pool && session.urls"
|
||||
>
|
||||
<MapProxiedArcgisTile
|
||||
:location="selectedTask.pool?.location"
|
||||
:markers="[]"
|
||||
:organizationId="session.organization.id"
|
||||
:tegola="session.urls?.tegola ?? ''"
|
||||
:urlTiles="session.urls?.tile ?? ''"
|
||||
@map-click="doPoolLocation"
|
||||
:markers="mapMarkers"
|
||||
:organizationId="session.organization.id"
|
||||
:tegola="session.urls!.tegola"
|
||||
:urlTiles="session.urls!.tile"
|
||||
v-model="_mapFlyoverCamera"
|
||||
></MapProxiedArcgisTile>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import MapLocator from "@/components/MapLocator.vue";
|
||||
import MapProxiedArcgisTile from "@/components/MapProxiedArcgisTile.vue";
|
||||
import { formatAddress } from "@/format";
|
||||
|
|
@ -167,12 +166,14 @@ import { Camera } from "@/type/map";
|
|||
interface Props {
|
||||
loading: boolean;
|
||||
mapBounds?: Bounds;
|
||||
mapFlyoverCamera: Camera;
|
||||
mapMarkers: Marker[];
|
||||
newPoolCondition: string;
|
||||
newPoolLocation: Location;
|
||||
selectedTask?: ReviewTask;
|
||||
}
|
||||
const mapCamera = ref<Camera>(new Camera());
|
||||
const _mapFlyoverCamera = ref<Camera>(new Camera());
|
||||
const props = defineProps<Props>();
|
||||
const poolCondition = ref<string>("unknown");
|
||||
const poolLocation = ref<Location>({
|
||||
|
|
@ -182,25 +183,14 @@ const poolLocation = ref<Location>({
|
|||
const siteOwner = ref<Contact>(new Contact());
|
||||
const siteResident = ref<Contact>(new Contact());
|
||||
const session = useSessionStore();
|
||||
const markers = computed((): Marker[] => {
|
||||
if (!poolLocation.value) {
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
poolLocation.value.latitude == 0.0 &&
|
||||
poolLocation.value.longitude == 0.0
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const marker = {
|
||||
color: "#FF0000",
|
||||
draggable: false,
|
||||
id: "x",
|
||||
location: poolLocation.value,
|
||||
};
|
||||
return [marker];
|
||||
});
|
||||
function doPoolLocation(event: MapClickEvent) {
|
||||
console.log("pool location", event);
|
||||
}
|
||||
watch(
|
||||
() => props.mapFlyoverCamera,
|
||||
(newMapFlyoverCamera: Camera) => {
|
||||
console.log("map flyover camera update", newMapFlyoverCamera);
|
||||
_mapFlyoverCamera.value = newMapFlyoverCamera;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { SSEManager, SSEMessage } from "@/SSEManager";
|
||||
import { ReviewTask } from "@/type/api";
|
||||
import { ReviewTask, ReviewTaskListResponse } from "@/type/api";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
|
||||
export const useReviewTaskStore = defineStore("review-task", () => {
|
||||
export const useStoreReviewTask = defineStore("review-task", () => {
|
||||
// State
|
||||
const _byID = ref<Map<number, ReviewTask>>(new Map());
|
||||
const loading = ref<boolean>(false);
|
||||
|
|
@ -23,7 +23,7 @@ export const useReviewTaskStore = defineStore("review-task", () => {
|
|||
function byID(id: number): ReviewTask | undefined {
|
||||
return _byID.value.get(id);
|
||||
}
|
||||
async function fetchAll(): Promise<void> {
|
||||
async function fetchAll(): Promise<ReviewTask[]> {
|
||||
const session = useSessionStore();
|
||||
if (session.urls == null) {
|
||||
throw new Error("can't fetch without user URL data");
|
||||
|
|
@ -41,11 +41,12 @@ export const useReviewTaskStore = defineStore("review-task", () => {
|
|||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const data: ReviewTaskListResponse = await response.json();
|
||||
_byID.value = new Map();
|
||||
for (const t of data.tasks) {
|
||||
_byID.value.set(t.id, t);
|
||||
}
|
||||
return data.tasks;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Unknown error";
|
||||
console.error("Error loading tasks:", err);
|
||||
|
|
|
|||
|
|
@ -553,6 +553,10 @@ export interface ReviewTask {
|
|||
pool?: ReviewTaskPool;
|
||||
id: number;
|
||||
}
|
||||
export interface ReviewTaskListResponse {
|
||||
tasks: ReviewTask[];
|
||||
total: number;
|
||||
}
|
||||
export interface UploadDTO {
|
||||
created: string;
|
||||
filename: string;
|
||||
|
|
|
|||
|
|
@ -62,11 +62,11 @@ body {
|
|||
<ThreeColumn>
|
||||
<template #left>
|
||||
<ReviewPoolColumnList
|
||||
v-if="reviewTask.all"
|
||||
v-if="storeReviewTask.all"
|
||||
@doSelectTask="selectTask"
|
||||
:error="error"
|
||||
:selectedTaskID="selectedTaskID"
|
||||
:tasks="reviewTask.all()"
|
||||
:tasks="storeReviewTask.all()"
|
||||
:total="totalPending"
|
||||
/>
|
||||
<div v-else>
|
||||
|
|
@ -77,6 +77,7 @@ body {
|
|||
<ReviewPoolColumnDetail
|
||||
:loading="loading"
|
||||
:mapBounds="mapBounds || undefined"
|
||||
:mapFlyoverCamera="mapFlyoverCamera"
|
||||
:mapMarkers="mapMarkers"
|
||||
:newPoolCondition="newPoolCondition"
|
||||
:newPoolLocation="newPoolLocation"
|
||||
|
|
@ -97,7 +98,7 @@ body {
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useReviewTaskStore } from "@/store/review-task";
|
||||
import { useStoreReviewTask } from "@/store/review-task";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import ThreeColumn from "@/components/layout/ThreeColumn.vue";
|
||||
|
|
@ -106,6 +107,7 @@ import ReviewPoolColumnDetail from "@/components/ReviewPoolColumnDetail.vue";
|
|||
import ReviewPoolColumnList from "@/components/ReviewPoolColumnList.vue";
|
||||
import type { Changes } from "@/types";
|
||||
import { Bounds, Contact, Location, ReviewTask } from "@/type/api";
|
||||
import { Camera } from "@/type/map";
|
||||
import { MapClickEvent, Marker } from "@/types";
|
||||
|
||||
interface FormData {
|
||||
|
|
@ -146,6 +148,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
});
|
||||
|
||||
// State
|
||||
const mapFlyoverCamera = ref<Camera>(new Camera());
|
||||
const newPoolCondition = ref<string>("");
|
||||
const newPoolLocation = ref<Location>({ latitude: 0, longitude: 0 });
|
||||
const newOwnerName = ref<string>("");
|
||||
|
|
@ -153,12 +156,11 @@ const newResidentName = ref<string>("");
|
|||
const error = ref<string | null>(null);
|
||||
const loading = ref<boolean>(true);
|
||||
const mapBounds = ref<Bounds | null>(null);
|
||||
const mapMarkers = ref<Marker[]>([]);
|
||||
const selectedTaskID = ref<number | null>(null);
|
||||
const submitting = ref<boolean>(false);
|
||||
const totalPending = ref<number>(0);
|
||||
|
||||
const reviewTask = useReviewTaskStore();
|
||||
const storeReviewTask = useStoreReviewTask();
|
||||
const session = useSessionStore();
|
||||
|
||||
// Refs for map components
|
||||
|
|
@ -215,22 +217,32 @@ const changes = computed<Changes>(() => {
|
|||
|
||||
return { updated, unchanged };
|
||||
});
|
||||
|
||||
const mapMarkers = computed<Marker[]>(() => {
|
||||
const task = selectedTask.value;
|
||||
const loc = task?.pool?.location;
|
||||
if (!loc) {
|
||||
return [];
|
||||
}
|
||||
const markers = {
|
||||
color: "#FF0000",
|
||||
draggable: false,
|
||||
id: "x",
|
||||
location: loc,
|
||||
};
|
||||
return [markers];
|
||||
});
|
||||
const selectedTask = computed<ReviewTask | undefined>(() => {
|
||||
if (selectedTaskID.value == null) {
|
||||
return undefined;
|
||||
}
|
||||
return reviewTask.byID(selectedTaskID.value);
|
||||
return storeReviewTask.byID(selectedTaskID.value);
|
||||
});
|
||||
async function fetchTasks() {
|
||||
await reviewTask.fetchAll();
|
||||
}
|
||||
// Helper Functions
|
||||
// Task Selection
|
||||
function selectTask(id: number): void {
|
||||
selectedTaskID.value = id;
|
||||
|
||||
const task = reviewTask.byID(id);
|
||||
const task = storeReviewTask.byID(id);
|
||||
if (!task) {
|
||||
console.log("no task", id);
|
||||
return;
|
||||
|
|
@ -241,6 +253,7 @@ function selectTask(id: number): void {
|
|||
return;
|
||||
}
|
||||
console.log("selecting task", id, task);
|
||||
mapFlyoverCamera.value = new Camera(pool.location, 15);
|
||||
newPoolCondition.value = pool.condition;
|
||||
newPoolLocation.value = pool.location;
|
||||
newOwnerName.value = pool.site.owner?.name ?? "";
|
||||
|
|
@ -262,14 +275,6 @@ function updateMap(task: ReviewTask): void {
|
|||
map.SetMarkers([]);
|
||||
return;
|
||||
}
|
||||
const markers = [
|
||||
new maplibregl.Marker({
|
||||
color: "#FF0000",
|
||||
draggable: false,
|
||||
}).setLngLat([loc.longitude, loc.latitude]),
|
||||
];
|
||||
|
||||
map.SetMarkers(markers);
|
||||
|
||||
const bounds = new maplibregl.LngLatBounds(
|
||||
new maplibregl.LngLat(loc.longitude - 0.005, loc.latitude - 0.005),
|
||||
|
|
@ -326,12 +331,20 @@ async function submitReview(action: "committed" | "discarded"): Promise<void> {
|
|||
if (!response.ok) {
|
||||
throw new Error("Failed to submit review");
|
||||
}
|
||||
// Save the current item's index for setting the newly selected item
|
||||
const index = storeReviewTask
|
||||
.all()
|
||||
.findIndex((t) => t.id == selectedTaskID.value);
|
||||
|
||||
// Remove task from list
|
||||
reviewTask.remove(selectedTask.value!.id);
|
||||
storeReviewTask.remove(selectedTask.value!.id);
|
||||
|
||||
// Update list of tasks
|
||||
await fetchTasks();
|
||||
const all_tasks: ReviewTask[] = await storeReviewTask.fetchAll();
|
||||
|
||||
// Select the next item in the list
|
||||
let new_index = index < all_tasks.length ? index : all_tasks.length - 1;
|
||||
selectedTaskID.value = all_tasks[new_index].id;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Unknown error";
|
||||
console.error("Error submitting review:", err);
|
||||
|
|
@ -449,6 +462,7 @@ function initializeMaps(): void {
|
|||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
initializeMaps();
|
||||
await fetchTasks();
|
||||
|
||||
await storeReviewTask.fetchAll();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue