Some checks failed
/ golint (push) Failing after 10s
These changes are meant to make it possible for new events that come in to immediately render in the components that depend on them.
310 lines
8.8 KiB
Vue
310 lines
8.8 KiB
Vue
<style scoped></style>
|
|
|
|
<template>
|
|
<ThreeColumn>
|
|
<template #header>
|
|
<div class="col">
|
|
<h3 class="mb-1">Communication Workbench</h3>
|
|
<div class="text-muted small">
|
|
Communications from various sources come in at the left, are
|
|
investigated in the center, and labeled as valuable signal or invalid
|
|
on the right.
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #left>
|
|
<CommunicationColumnList
|
|
:all="visibleCommunications"
|
|
@deselect="handleDeselect"
|
|
:loading="storeResource.communication.loadingAll()"
|
|
:selectedID="selectedId"
|
|
@select="handleSelect"
|
|
/>
|
|
</template>
|
|
<template #center>
|
|
<CommunicationColumnDetail
|
|
:loading="storePublicReport.loading || loadingSelectedCommunication"
|
|
:mapBounds="mapBounds || undefined"
|
|
:mapMarkers="mapMarkers"
|
|
:selectedCommunication="selectedCommunication"
|
|
:selectedReport="selectedReport"
|
|
@viewImage="openImageViewer"
|
|
/>
|
|
</template>
|
|
<template #right>
|
|
<CommunicationColumnAction
|
|
:isLoading="storePublicReport.loading || loadingSelectedCommunication"
|
|
@markInvalid="markInvalid"
|
|
@markPendingResponse="markPendingResponse"
|
|
@markPossibleIssue="markPossibleIssue"
|
|
@markPossibleResolved="markPossibleResolved"
|
|
@sendMessage="sendMessage"
|
|
:selectedCommunication="selectedCommunication"
|
|
:selectedReport="selectedReport"
|
|
/>
|
|
</template>
|
|
</ThreeColumn>
|
|
<ImageViewerModal
|
|
@close="showImageModal = false"
|
|
@imageNext="imageNext()"
|
|
@imagePrevious="imagePrevious()"
|
|
:images="currentImages"
|
|
:currentImageIndex="currentImageIndex"
|
|
:show="showImageModal"
|
|
/>
|
|
<ToastNotification
|
|
:message="toastMessage"
|
|
:show="toastShow"
|
|
:title="toastTitle"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref, watch } from "vue";
|
|
import { computedAsync } from "@vueuse/core";
|
|
import maplibregl from "maplibre-gl";
|
|
|
|
import { apiClient } from "@/client";
|
|
import CommunicationColumnAction from "@/components/CommunicationColumnAction.vue";
|
|
import CommunicationColumnDetail from "@/components/CommunicationColumnDetail.vue";
|
|
import CommunicationColumnList from "@/components/CommunicationColumnList.vue";
|
|
import ImageViewerModal from "@/components/ImageViewerModal.vue";
|
|
import ThreeColumn from "@/components/layout/ThreeColumn.vue";
|
|
import ToastNotification from "@/components/ToastNotification.vue";
|
|
import { useQueryParam } from "@/composable/use-query-param";
|
|
import { log } from "@/log";
|
|
import { SSEManager } from "@/SSEManager";
|
|
import { useStoreResource } from "@/store/resource";
|
|
import { useSessionStore } from "@/store/session";
|
|
import type { Marker } from "@/types";
|
|
import { Bounds, type Communication, PublicReport } from "@/type/api";
|
|
import type { LngLatBounds } from "@/map/Map.vue";
|
|
import { boundsForServiceArea, boundsWithPadding } from "@/map/util";
|
|
import { useStorePublicReport } from "@/store/publicreport";
|
|
|
|
const session = useSessionStore();
|
|
|
|
// Refs
|
|
const currentImageIndex = ref<number>(0);
|
|
const paramCommunication = useQueryParam("communication");
|
|
const selectedId = ref<string | undefined>(undefined);
|
|
const showImageModal = ref(false);
|
|
const storePublicReport = useStorePublicReport();
|
|
const storeResource = useStoreResource();
|
|
const toastMessage = ref("");
|
|
const toastShow = ref(false);
|
|
const toastTitle = ref("");
|
|
|
|
const currentImage = computed(() => {
|
|
const comm = selectedCommunication.value;
|
|
return selectedReport.value?.images[currentImageIndex.value] ?? null;
|
|
});
|
|
const currentImages = computed(() => {
|
|
const comm = selectedCommunication.value;
|
|
if (comm == null) {
|
|
return [];
|
|
}
|
|
return selectedReport.value?.images ?? [];
|
|
});
|
|
const loadingSelectedCommunication = computed<boolean>((): boolean => {
|
|
return !!selectedCommunication.value;
|
|
});
|
|
const mapBounds = computed<LngLatBounds | null>((): LngLatBounds | null => {
|
|
let bounds = new Bounds();
|
|
const loc = selectedReport.value?.location;
|
|
log.info("updating for loc", loc);
|
|
if (loc && loc.latitude != 0 && loc.longitude != 0) {
|
|
bounds.addLocation(loc);
|
|
}
|
|
const address_loc = selectedReport.value?.address.location;
|
|
if (address_loc && address_loc.latitude != 0 && address_loc.longitude != 0) {
|
|
bounds.addLocation(address_loc);
|
|
}
|
|
|
|
for (const [i, image] of (selectedReport.value?.images ?? []).entries()) {
|
|
if (
|
|
image.location != null &&
|
|
image.location.latitude != 0 &&
|
|
image.location.longitude != 0
|
|
) {
|
|
bounds.addLocation(image.location);
|
|
}
|
|
}
|
|
if (bounds.isEmpty()) {
|
|
return boundsForServiceArea();
|
|
}
|
|
return boundsWithPadding(bounds.min, bounds.max, 0.01);
|
|
});
|
|
const mapMarkers = computed<Marker[]>((): Marker[] => {
|
|
const loc = selectedReport.value?.location;
|
|
let markers: Marker[] = [];
|
|
if (loc && loc.latitude != 0 && loc.longitude != 0) {
|
|
markers.push({
|
|
color: "#0000FF",
|
|
draggable: false,
|
|
id: "reporter",
|
|
location: loc,
|
|
});
|
|
}
|
|
const address_loc = selectedReport.value?.address.location;
|
|
if (address_loc && address_loc.latitude != 0 && address_loc.longitude != 0) {
|
|
markers.push({
|
|
color: "#FF0000",
|
|
draggable: false,
|
|
id: "address",
|
|
location: address_loc,
|
|
});
|
|
}
|
|
|
|
for (const [i, image] of (selectedReport.value?.images ?? []).entries()) {
|
|
if (
|
|
image.location != null &&
|
|
image.location.latitude != 0 &&
|
|
image.location.longitude != 0
|
|
) {
|
|
markers.push({
|
|
color: "#00FF00",
|
|
draggable: false,
|
|
id: `image-${i}`,
|
|
location: image.location,
|
|
});
|
|
}
|
|
}
|
|
return markers;
|
|
});
|
|
const selectedCommunication = computed((): Communication | undefined => {
|
|
log.info("get selectedCommunication", selectedId.value);
|
|
if (!selectedId.value) {
|
|
return undefined;
|
|
}
|
|
const all = Array.from(storeResource.communication.byURI.values());
|
|
const result = all.find((c: Communication) => c.id == selectedId.value);
|
|
log.info("selectedCommunication", selectedId.value, result);
|
|
return result;
|
|
});
|
|
const selectedReport = computedAsync(
|
|
async (): Promise<PublicReport | undefined> => {
|
|
if (
|
|
!(
|
|
selectedCommunication.value &&
|
|
selectedCommunication.value.type != "publicreport"
|
|
)
|
|
)
|
|
return;
|
|
return await storePublicReport.byURI(selectedCommunication.value.source);
|
|
},
|
|
);
|
|
const visibleCommunications = computed((): Communication[] | undefined => {
|
|
log.info("get visibleCommunications", storeResource.communication.byURI);
|
|
if (!storeResource.communication.byURI) {
|
|
return undefined;
|
|
}
|
|
const all: Communication[] = Array.from(
|
|
storeResource.communication.byURI.values(),
|
|
);
|
|
const result = all.filter((c: Communication) => {
|
|
return c.status == "new" || c.status == "opened";
|
|
});
|
|
log.info("visibleCommunications:", result);
|
|
return result;
|
|
});
|
|
const handleDeselect = (id: string) => {
|
|
selectedId.value = undefined;
|
|
};
|
|
const handleSelect = (id: string) => {
|
|
selectedId.value = id;
|
|
paramCommunication.setValue(id);
|
|
};
|
|
function imageNext() {
|
|
currentImageIndex.value = Math.min(
|
|
currentImages.value.length - 1,
|
|
currentImageIndex.value + 1,
|
|
);
|
|
}
|
|
function imagePrevious() {
|
|
currentImageIndex.value = Math.max(0, currentImageIndex.value - 1);
|
|
}
|
|
|
|
function openImageViewer(index: number) {
|
|
currentImageIndex.value = index;
|
|
showImageModal.value = true;
|
|
}
|
|
|
|
async function markInvalid() {
|
|
markReport("Invalid", "invalid");
|
|
}
|
|
async function markPendingResponse() {
|
|
markReport("Pending Response", "pending-response");
|
|
}
|
|
async function markPossibleIssue() {
|
|
markReport("Possible Issue", "possible-issue");
|
|
}
|
|
async function markPossibleResolved() {
|
|
markReport("Possibly Resolved", "possible-resolved");
|
|
}
|
|
async function markReport(title: string, status: string) {
|
|
if (selectedCommunication.value == null) {
|
|
return;
|
|
}
|
|
const url = `${selectedCommunication.value.uri}/mark/${status}`;
|
|
const result = apiClient.JSONPost(url, {});
|
|
|
|
showNotification(
|
|
`Report Marked ${title}`,
|
|
`Report #${selectedCommunication.value.id} has been updated`,
|
|
);
|
|
await storeResource.communication.fetchAll();
|
|
}
|
|
|
|
async function sendMessage(message: string) {
|
|
if (!message.trim()) return;
|
|
if (selectedCommunication.value == null) return;
|
|
if (selectedReport.value == null) return;
|
|
if (session.urls == null) return;
|
|
log.info("Sending message reporter:", message);
|
|
|
|
const payload = {
|
|
message: message,
|
|
reportID: selectedCommunication.value.id,
|
|
};
|
|
const response = await fetch(session.urls?.api.publicreport_message, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
showNotification(
|
|
"Message Sent",
|
|
`Message successfully sent to ${selectedReport.value.reporter.name}`,
|
|
);
|
|
}
|
|
function showNotification(title: string, message: string) {
|
|
toastTitle.value = title;
|
|
toastMessage.value = message;
|
|
toastShow.value = true;
|
|
|
|
setTimeout(() => {
|
|
toastShow.value = false;
|
|
}, 3000);
|
|
}
|
|
|
|
// Lifecycle hooks
|
|
onMounted(async () => {
|
|
await storeResource.communication.fetchAll();
|
|
});
|
|
watch(
|
|
paramCommunication.value,
|
|
(communication_id) => {
|
|
if (communication_id) {
|
|
handleSelect(communication_id);
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
</script>
|