Load communication reports asynchronously

This solves some problems created by making the publicreport part of the
communication API consistent. There are a lot of optimizations still on
the table with this one, but for now I need to get this out.
This commit is contained in:
Eli Ribble 2026-04-28 14:49:02 +00:00
parent 6e3d079c46
commit b68d93ec91
No known key found for this signature in database
12 changed files with 184 additions and 233 deletions

View file

@ -7,7 +7,44 @@ import (
"github.com/gorilla/mux"
)
func AddRoutes(r *mux.Router) {
func AddRoutesRMO(r *mux.Router) {
router := resource.NewRouter(r)
compliance_request := resource.ComplianceRequest(router)
district := resource.District(router)
geocode := resource.Geocode(router)
nuisance := resource.Nuisance(router)
pr_compliance := resource.PublicReportCompliance(router)
publicreport := resource.Publicreport(router)
publicreport_notification := resource.PublicreportNotification(router)
qrcode := resource.QRCode(router)
water := resource.Water(router)
r.HandleFunc("/compliance-request/image/pool/{public_id}", compliance_request.ImagePoolGet).Methods("GET").Name("compliance-request.image.pool.ByIDGet")
r.Handle("/district", handlerJSONSlice(district.List)).Methods("GET")
r.Handle("/district/{id}", handlerJSON(district.GetByID)).Methods("GET").Name("district.ByIDGet")
r.HandleFunc("/district/{slug}/logo", apiGetDistrictLogo).Methods("GET").Name("district.logo.BySlug")
r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET")
r.Handle("/geocode/reverse", handlerJSONPost(geocode.Reverse)).Methods("POST")
r.Handle("/geocode/reverse/closest", handlerJSONPost(geocode.ReverseClosest)).Methods("POST")
r.Handle("/geocode/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET")
r.Handle("/publicreport-notification", handlerJSONPost(publicreport_notification.Create)).Methods("POST")
r.Handle("/qr-code/mailer/{code}", handlerBasic(qrcode.Mailer)).Methods("GET")
r.Handle("/qr-code/marketing", handlerBasic(qrcode.Marketing)).Methods("GET")
r.Handle("/qr-code/report/{code}", handlerBasic(qrcode.Report)).Methods("GET")
r.HandleFunc("/rmo/compliance", handlerJSONPost(pr_compliance.Create)).Methods("POST")
r.HandleFunc("/rmo/nuisance", handlerFormPost(nuisance.Create)).Methods("POST")
r.Handle("/rmo/publicreport/{id}", handlerBasic(publicreport.ByIDPublic)).Methods("GET").Name("publicreport.ByIDGetPublic")
r.Handle("/rmo/publicreport/compliance/{id}/image", handlerFormPost(publicreport.ImageCreate)).Methods("POST")
r.Handle("/rmo/publicreport/compliance/{id}", handlerJSON(pr_compliance.ByIDPublic)).Methods("GET").Name("publicreport.compliance.ByIDGetPublic")
r.Handle("/rmo/publicreport/compliance/{id}", handlerJSONPut(pr_compliance.Update)).Methods("PUT")
r.Handle("/rmo/publicreport/nuisance/{id}", handlerJSON(nuisance.ByIDPublic)).Methods("GET").Name("publicreport.nuisance.ByIDGetPublic")
r.Handle("/rmo/publicreport/water/{id}", handlerJSON(water.ByIDPublic)).Methods("GET").Name("publicreport.water.ByIDGetPublic")
r.Handle("/rmo/publicreport/{id}", handlerBasic(publicreport.ByIDPublic)).Methods("GET").Name("publicreport.ByIDGetPublicPublic")
r.HandleFunc("/rmo/water", handlerFormPost(water.Create)).Methods("POST")
}
func AddRoutesSync(r *mux.Router) {
router := resource.NewRouter(r)
compliance_request := resource.ComplianceRequest(router)
@ -39,16 +76,6 @@ func AddRoutes(r *mux.Router) {
r.Handle("/qr-code/mailer/{code}", handlerBasic(qrcode.Mailer)).Methods("GET")
r.Handle("/qr-code/marketing", handlerBasic(qrcode.Marketing)).Methods("GET")
r.Handle("/qr-code/report/{code}", handlerBasic(qrcode.Report)).Methods("GET")
r.HandleFunc("/rmo/compliance", handlerJSONPost(pr_compliance.Create)).Methods("POST")
r.HandleFunc("/rmo/nuisance", handlerFormPost(nuisance.Create)).Methods("POST")
r.Handle("/rmo/publicreport/{id}", handlerBasic(publicreport.ByIDPublic)).Methods("GET").Name("publicreport.ByIDGetPublic")
r.Handle("/rmo/publicreport/compliance/{id}/image", handlerFormPost(publicreport.ImageCreate)).Methods("POST")
r.Handle("/rmo/publicreport/compliance/{id}", handlerJSON(pr_compliance.ByIDPublic)).Methods("GET").Name("publicreport.compliance.ByIDGetPublic")
r.Handle("/rmo/publicreport/compliance/{id}", handlerJSONPut(pr_compliance.Update)).Methods("PUT")
r.Handle("/rmo/publicreport/nuisance/{id}", handlerJSON(nuisance.ByIDPublic)).Methods("GET").Name("publicreport.nuisance.ByIDGetPublic")
r.Handle("/rmo/publicreport/water/{id}", handlerJSON(water.ByIDPublic)).Methods("GET").Name("publicreport.water.ByIDGetPublic")
r.Handle("/rmo/publicreport/{id}", handlerBasic(publicreport.ByIDPublic)).Methods("GET").Name("publicreport.ByIDGetPublicPublic")
r.HandleFunc("/rmo/water", handlerFormPost(water.Create)).Methods("POST")
r.HandleFunc("/signin", handlerJSONPost(postSignin))
r.Handle("/signout", authenticatedHandlerBasic(postSignout))
r.HandleFunc("/signup", handlerJSONPost(postSignup))

View file

@ -122,16 +122,15 @@ func main() {
r.Use(sentryMiddleware.Handle)
r.Use(auth.NewSessionManager().LoadAndSave)
sync_router := r.Host(config.DomainNidus).Subrouter()
rmo_router := r.Host(config.DomainRMO).Subrouter()
// Set up routing by hostname
sync_router := r.Host(config.DomainNidus).Subrouter()
sync_api_router := sync_router.PathPrefix("/api").Subrouter()
api.AddRoutes(sync_api_router)
api.AddRoutesSync(sync_api_router)
nidussync.Router(sync_router)
rmo_router := r.Host(config.DomainRMO).Subrouter()
rmo_api_router := rmo_router.PathPrefix("/api").Subrouter()
api.AddRoutes(rmo_api_router)
api.AddRoutesRMO(rmo_api_router)
rmo.Router(rmo_router)
//hr.Map("", sr) // default

View file

@ -9,7 +9,6 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/config"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/google/uuid"
//"github.com/gorilla/mux"
//"github.com/rs/zerolog/log"
@ -26,10 +25,10 @@ func Communication(r *router) *communicationR {
}
type communication struct {
Created time.Time `json:"created"`
ID string `json:"id"`
PublicReport types.PublicReport `json:"public_report"`
Type string `json:"type"`
Created time.Time `json:"created"`
ID string `json:"id"`
PublicReport string `json:"public_report"`
Type string `json:"type"`
}
type communicationList struct {
Communications []communication `json:"communications"`
@ -58,7 +57,7 @@ func (res *communicationR) List(ctx context.Context, r *http.Request, user platf
comms[i] = communication{
Created: report.Created,
ID: report.PublicID,
PublicReport: *report,
PublicReport: report.URI,
Type: "publicreport." + string(report.Type),
}
}

View file

@ -49,8 +49,8 @@
<div
v-if="
!(
selectedCommunication?.public_report?.reporter.has_email ||
selectedCommunication?.public_report?.reporter.has_phone
selectedReport?.reporter.has_email ||
selectedReport?.reporter.has_phone
)
"
class="mb-3"
@ -62,8 +62,8 @@
</div>
<div
v-if="
selectedCommunication?.public_report?.reporter.has_email ||
selectedCommunication?.public_report?.reporter.has_phone
selectedReport?.reporter.has_email ||
selectedReport?.reporter.has_phone
"
class="mb-3"
>
@ -107,8 +107,7 @@
<h6><i class="bi bi-clock-history"></i> Activity Log</h6>
<div class="small">
<div
v-for="(entry, index) in selectedCommunication?.public_report
?.log || []"
v-for="(entry, index) in selectedReport?.log || []"
:key="index"
class="border-start border-2 ps-2 mb-2"
>
@ -116,8 +115,7 @@
</div>
<div
v-if="
!selectedCommunication?.public_report?.log ||
selectedCommunication?.public_report?.log.length === 0
!selectedReport?.log || selectedReport?.log.length === 0
"
class="text-muted"
>
@ -134,7 +132,7 @@
<script setup lang="ts">
import { ref } from "vue";
import { Communication, User } from "@/type/api";
import { Communication, PublicReport, User } from "@/type/api";
import ListCardActivityLog from "@/components/ListCardActivityLog.vue";
interface Emits {
(e: "markSignal"): void;
@ -144,6 +142,7 @@ interface Emits {
interface Props {
loading: boolean;
selectedCommunication: Communication | null;
selectedReport: PublicReport | undefined;
}
const emit = defineEmits<Emits>();
@ -151,10 +150,10 @@ const messageText = ref("");
const props = withDefaults(defineProps<Props>(), {});
function applyMessageTemplate(template: string) {
const templates = {
received: `Dear ${props.selectedCommunication?.public_report?.reporter.name || "Resident"},\n\nThank you for submitting your report to the Mosquito Control District. We have received your communication and it has been assigned to our team for review.\n\nWe will be in touch if we need any additional information.\n\nBest regards,\nMosquito Control District`,
scheduled: `Dear ${props.selectedCommunication?.public_report?.reporter.name || "Resident"},\n\nGood news! Based on your report, we have scheduled a service visit to your area. Our technicians will be conducting mosquito control operations within the next 3-5 business days.\n\nNo action is required on your part.\n\nBest regards,\nMosquito Control District`,
completed: `Dear ${props.selectedCommunication?.public_report?.reporter.name || "Resident"},\n\nWe wanted to let you know that our team has completed mosquito control operations in your area based on your recent report.\n\nIf you continue to experience issues, please don't hesitate to submit a new report.\n\nBest regards,\nMosquito Control District`,
need_info: `Dear ${props.selectedCommunication?.public_report?.reporter.name || "Resident"},\n\nThank you for your recent report. In order to better assist you, we need some additional information:\n\n- [Specify what information is needed]\n\nPlease reply to this message with the requested details.\n\nBest regards,\nMosquito Control District`,
received: `Dear ${props.selectedReport?.reporter.name || "Resident"},\n\nThank you for submitting your report to the Mosquito Control District. We have received your communication and it has been assigned to our team for review.\n\nWe will be in touch if we need any additional information.\n\nBest regards,\nMosquito Control District`,
scheduled: `Dear ${props.selectedReport?.reporter.name || "Resident"},\n\nGood news! Based on your report, we have scheduled a service visit to your area. Our technicians will be conducting mosquito control operations within the next 3-5 business days.\n\nNo action is required on your part.\n\nBest regards,\nMosquito Control District`,
completed: `Dear ${props.selectedReport?.reporter.name || "Resident"},\n\nWe wanted to let you know that our team has completed mosquito control operations in your area based on your recent report.\n\nIf you continue to experience issues, please don't hesitate to submit a new report.\n\nBest regards,\nMosquito Control District`,
need_info: `Dear ${props.selectedReport?.reporter.name || "Resident"},\n\nThank you for your recent report. In order to better assist you, we need some additional information:\n\n- [Specify what information is needed]\n\nPlease reply to this message with the requested details.\n\nBest regards,\nMosquito Control District`,
};
if (template in templates) {

View file

@ -37,61 +37,49 @@
<div class="card shadow-sm mb-3">
<div class="card-header bg-white pane-header">Communication Workbench</div>
<div class="card-body">
<div
v-if="loading || session.self == null || session.organization == null"
class="loading"
>
Loading...
</div>
<div v-else>
<div class="map-container">
<Map
:bounds="mapBounds"
:markers="mapMarkers"
:organizationId="session.organization?.id"
>
<Layer
id="parcel"
:minzoom="14"
:paint="{ 'line-color': '#0f0' }"
source="tegola"
sourceLayer="parcel"
type="line"
/>
<Layer
id="service-area"
:paint="{ 'line-color': '#f00' }"
source="tegola"
sourceLayer="service-area-bounds"
type="line"
/>
<Source
id="tegola"
type="vector"
:tiles="[
session.urls?.tegola +
'maps/nidus/{z}/{x}/{y}?id=' +
session.organization?.id,
]"
/>
</Map>
</div>
<div
v-if="!selectedCommunication"
class="d-flex flex-column align-items-center justify-content-center text-muted"
>
<i class="bi bi-hand-index fs-1"></i>
<p class="mt-2">Select a report to view details</p>
</div>
<div v-if="selectedCommunication" class="h-100 d-flex flex-column">
<PublicReportCard
v-if="selectedCommunication?.public_report"
:report="selectedCommunication?.public_report"
@viewImage="openPhotoViewer"
<div class="map-container">
<Map :bounds="mapBounds" :markers="mapMarkers">
<Layer
id="parcel"
:minzoom="14"
:paint="{ 'line-color': '#0f0' }"
source="tegola"
sourceLayer="parcel"
type="line"
/>
<p v-else>No public report</p>
</div>
<Layer
id="service-area"
:paint="{ 'line-color': '#f00' }"
source="tegola"
sourceLayer="service-area-bounds"
type="line"
/>
<Source
id="tegola"
type="vector"
:tiles="[
session.urls?.tegola +
'maps/nidus/{z}/{x}/{y}?id=' +
session.organization?.id,
]"
/>
</Map>
</div>
<div
v-if="!selectedCommunication"
class="d-flex flex-column align-items-center justify-content-center text-muted"
>
<i class="bi bi-hand-index fs-1"></i>
<p class="mt-2">Select a report to view details</p>
</div>
<div class="h-100 d-flex flex-column" v-else>
<PublicReportCard
v-if="selectedReport"
:report="selectedReport"
@viewImage="openPhotoViewer"
/>
<p v-else>Loading details...</p>
</div>
</div>
</div>
@ -99,14 +87,15 @@
<script setup lang="ts">
import { computed } from "vue";
import PublicReportCard from "@/components/PublicReportCard.vue";
import TimeRelative from "@/components/TimeRelative.vue";
import Map, { LngLatBounds } from "@/map/Map.vue";
import Layer from "@/map/Layer.vue";
import Source from "@/map/Source.vue";
import PublicReportCard from "@/components/PublicReportCard.vue";
import TimeRelative from "@/components/TimeRelative.vue";
import type { Marker } from "@/types";
import type { Bounds, Communication, User } from "@/type/api";
import { useSessionStore } from "@/store/session";
import type { Marker } from "@/types";
import type { Bounds, Communication, PublicReport, User } from "@/type/api";
interface Emits {
(e: "viewImage", index: number): void;
@ -116,6 +105,7 @@ interface Props {
mapBounds?: LngLatBounds;
mapMarkers: Marker[];
selectedCommunication: Communication | null;
selectedReport: PublicReport | undefined;
}
const emit = defineEmits<Emits>();

View file

@ -61,12 +61,14 @@
</div>
<div class="list-group list-group-flush">
<div v-if="loading || all == null" class="loading">Loading...</div>
<div class="loading list-group-item" v-if="loading || all == null">
Loading...
</div>
<div
v-else-if="all.length > 0"
v-for="comm in filteredCommunications"
:key="comm.id"
class="border rounded list-group-item report-card p-3"
class="list-group-item report-card p-3"
:class="{
active: selectedId && selectedId === comm.id,
}"
@ -133,36 +135,6 @@ const filteredCommunications = computed((): Communication[] => {
if (props.all == null) {
return [];
}
return props.all.filter((c) => {
const matchesType =
typeFilter.value === "all" || c.type === typeFilter.value;
return matchesType && filterMatches(searchFilter.value, c);
});
return props.all;
});
// Methods
function filterMatches(filter: string, comm: Communication) {
const pr = comm.public_report;
// When we have non-public-report communications fix this.
if (pr == null) {
return false;
}
return filterMatchesPublicReport(filter, pr);
}
function filterMatchesLogEntry(filter: string, logs: LogEntry[]) {
for (const le of logs) {
if (le.message.includes(filter)) {
return true;
}
}
}
function filterMatchesPublicReport(filter: string, pr: PublicReport) {
if (
pr.address.raw.includes(filter) ||
pr.public_id.includes(filter) ||
filterMatchesLogEntry(filter, pr.log)
) {
return true;
}
return false;
}
</script>

View file

@ -1,33 +1,17 @@
<template>
<!-- First row: icon, type badge, and time -->
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<i class="bi fs-4 me-2" :class="iconForType()"></i>
<span class="badge" :class="colorForType()">
{{ titleForType() }}
</span>
<div class="row">
<div class="d-flex align-items-center">
<i class="bi fs-4 me-2" :class="iconForType()"></i>
<span class="badge" :class="colorForType()">
{{ titleForType() }}
</span>
</div>
</div>
<small>
<TimeRelative :time="comm.created" />
</small>
</div>
<!-- Details section: full width -->
<div>
<div>
<i class="bi bi-geo-alt text-muted"></i>
<span class="fw-medium">{{
comm.public_report?.address.postal_code
}}</span>
</div>
<small>{{ formatAddress(comm.public_report?.address) }}</small>
<div
v-if="comm.public_report?.images && comm.public_report?.images.length > 0"
class="mt-1"
>
<small class="text-muted">
<i class="bi bi-camera"></i>
{{ comm.public_report.images.length }} photo(s)
<div class="row">
<small>
<TimeRelative :time="comm.created" />
</small>
</div>
</div>

View file

@ -28,6 +28,7 @@ import {
watch,
} from "vue";
import { boundsDefault } from "@/map/util";
import type { MapClickEvent, Marker } from "@/types";
import type { Location } from "@/type/api";
@ -46,6 +47,7 @@ interface Props {
const emit = defineEmits<Emits>();
const props = withDefaults(defineProps<Props>(), {
bounds: boundsDefault,
cursor: "",
markers: () => [],
});
@ -145,7 +147,7 @@ provide("unregisterLayer", (id: string) => {
function initializeMap() {
if (!mapDiv.value) return;
console.log("initializing map...");
console.log("initializing map...", props.bounds, props.center, props.zoom);
const _map = new maplibregl.Map({
bounds: props.bounds,
container: mapDiv.value,

View file

@ -1,8 +1,10 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { Communication, CommunicationDTO } from "@/type/api";
import { apiClient } from "@/client";
import { SSEManager, SSEMessage } from "@/SSEManager";
import { useSessionStore } from "@/store/session";
import { Communication, CommunicationDTO } from "@/type/api";
export const useCommunicationStore = defineStore("communication", () => {
// State
@ -30,14 +32,9 @@ export const useCommunicationStore = defineStore("communication", () => {
params.append("sort", "-created");
//if (typeFilter.value) params.append("type", typeFilter.value);
const response = await fetch(
`${session.urls.api.communication}?${params}`,
);
const url = `${session.urls.api.communication}?${params}`;
const data = await apiClient.JSONGet(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
all.value = data.communications.map((c: CommunicationDTO) =>
Communication.fromJSON(c),
);
@ -45,11 +42,14 @@ export const useCommunicationStore = defineStore("communication", () => {
} catch (err) {
console.error("Error loading communications:", err);
throw err;
} finally {
loading.value = false;
}
}
return {
// State
all,
loading,
// Actions
fetchAll,
};

View file

@ -63,6 +63,8 @@ export const useStorePublicReport = defineStore("publicreport", () => {
} catch (err) {
console.error("Error loading users:", err);
throw err;
} finally {
loading.value = false;
}
}
async function update(
@ -81,5 +83,7 @@ export const useStorePublicReport = defineStore("publicreport", () => {
fetchByID,
fetchByURI,
update,
// State
loading,
};
});

View file

@ -510,7 +510,7 @@ export class PublicReportWater extends PublicReport {
export interface CommunicationDTO {
created: string;
id: string;
public_report?: PublicReportDTO;
public_report?: string;
type: string;
}
export class Communication {
@ -518,16 +518,14 @@ export class Communication {
public created: Date,
public id: string,
public type: string,
public public_report?: PublicReport,
public public_report?: string,
) {}
static fromJSON(json: CommunicationDTO): Communication {
return new Communication(
new Date(json.created),
json.id,
json.type,
json.public_report == undefined
? undefined
: PublicReport.fromJSON(json.public_report),
json.public_report,
);
}
}

View file

@ -14,29 +14,31 @@
</template>
<template #left>
<CommunicationColumnList
:all="communication.all"
:all="storeCommunication.all"
@deselect="handleDeselect"
:loading="loading"
:loading="storeCommunication.loading"
:selected-id="selectedId"
@select="handleSelect"
/>
</template>
<template #center>
<CommunicationColumnDetail
:loading="loading"
:loading="storePublicReport.loading || storeCommunication.loading"
:mapBounds="mapBounds || undefined"
:mapMarkers="mapMarkers"
:selectedCommunication="selectedCommunication"
:selectedReport="selectedReport"
@viewImage="openImageViewer"
/>
</template>
<template #right>
<CommunicationColumnAction
:loading="loading"
:loading="storePublicReport.loading || storeCommunication.loading"
@markInvalid="markInvalid"
@markSignal="markSignal"
@sendMessage="sendMessage"
:selectedCommunication="selectedCommunication"
:selectedReport="selectedReport"
/>
</template>
</ThreeColumn>
@ -57,6 +59,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { computedAsync } from "@vueuse/core";
import maplibregl from "maplibre-gl";
import CommunicationColumnAction from "@/components/CommunicationColumnAction.vue";
@ -69,52 +72,61 @@ import { SSEManager } from "@/SSEManager";
import { useCommunicationStore } from "@/store/communication";
import { useSessionStore } from "@/store/session";
import type { Marker } from "@/types";
import type { Communication } from "@/type/api";
import { Bounds } from "@/type/api";
import { Bounds, type Communication, PublicReport } from "@/type/api";
import type { LngLatBounds } from "@/map/Map.vue";
import { boundsWithPadding } from "@/map/util";
import { useStorePublicReport } from "@/store/publicreport";
const communication = useCommunicationStore();
const session = useSessionStore();
onMounted(() => {
fetchCommunications();
});
// Refs
const currentImageIndex = ref<number>(0);
const error = ref<string | null>(null);
const loading = ref<boolean>(true);
const mapBounds = ref<LngLatBounds | null>(null);
const mapMarkers = ref<Marker[]>([]);
const selectedId = ref<string | null>(null);
const showImageModal = ref(false);
const storeCommunication = useCommunicationStore();
const storePublicReport = useStorePublicReport();
const toastMessage = ref("");
const toastShow = ref(false);
const toastTitle = ref("");
const currentImage = computed(() => {
const comm = selectedCommunication.value;
return comm?.public_report?.images[currentImageIndex.value] ?? null;
return selectedReport.value?.images[currentImageIndex.value] ?? null;
});
const currentImages = computed(() => {
const comm = selectedCommunication.value;
if (comm == null || comm.public_report == null) {
return [];
}
return comm.public_report.images ?? [];
return selectedReport.value?.images ?? [];
});
const selectedCommunication = computed<Communication | null>(
(): Communication | null => {
if (selectedId.value == null) {
return null;
}
if (communication.all == null) {
if (storeCommunication.all == null) {
return null;
}
const result = communication.all.find((c) => c.id == selectedId.value);
const result = storeCommunication.all.find((c) => c.id == selectedId.value);
return result || null;
},
);
const selectedReport = computedAsync(
async (): Promise<PublicReport | undefined> => {
if (
!(
selectedCommunication.value && selectedCommunication.value.public_report
)
)
return;
return await storePublicReport.fetchByURI(
selectedCommunication.value.public_report,
);
},
);
const handleDeselect = (id: string) => {
selectedId.value = null;
updateMap();
@ -123,9 +135,6 @@ const handleSelect = (id: string) => {
selectedId.value = id;
updateMap();
};
async function fetchCommunications() {
await communication.fetchAll();
}
function imageNext() {
currentImageIndex.value = Math.min(
currentImages.value.length - 1,
@ -135,18 +144,6 @@ function imageNext() {
function imagePrevious() {
currentImageIndex.value = Math.max(0, currentImageIndex.value - 1);
}
async function loadFromAPI() {
loading.value = true;
error.value = null;
try {
await Promise.all([fetchCommunications()]);
} catch (err) {
error.value = err instanceof Error ? err.message : "fetch error";
console.error("Error loading data:", err);
} finally {
loading.value = false;
}
}
function openImageViewer(index: number) {
currentImageIndex.value = index;
@ -174,7 +171,7 @@ async function markInvalid() {
`Report #${selectedCommunication.value.id} has been marked as invalid`,
);
removeCurrentFromList();
await fetchCommunications();
await storeCommunication.fetchAll();
}
async function markSignal() {
@ -202,24 +199,25 @@ async function markSignal() {
"Report Marked Signal",
`Report #${report_id} has been marked as useful signal`,
);
await fetchCommunications();
await storeCommunication.fetchAll();
} catch (err) {
error.value = err instanceof Error ? err.message : "fetch error";
console.error("Error creating lead:", err);
}
}
function removeCurrentFromList() {
if (communication.all == null) {
if (storeCommunication.all == null) {
return;
}
const index = communication.all.findIndex((c) => c.id === selectedId.value);
const index = storeCommunication.all.findIndex(
(c) => c.id === selectedId.value,
);
if (index > -1) {
communication.all.splice(index, 1);
storeCommunication.all.splice(index, 1);
}
if (communication.all.length > 0) {
const nextIndex = Math.min(index, communication.all.length - 1);
selectedId.value = communication.all[nextIndex].id;
if (storeCommunication.all.length > 0) {
const nextIndex = Math.min(index, storeCommunication.all.length - 1);
selectedId.value = storeCommunication.all[nextIndex].id;
} else {
selectedId.value = null;
}
@ -227,6 +225,7 @@ function removeCurrentFromList() {
async function sendMessage(message: string) {
if (!message.trim()) return;
if (selectedCommunication.value == null) return;
if (selectedReport.value == null) return;
if (session.urls == null) return;
console.log("Sending message reporter:", message);
@ -248,7 +247,7 @@ async function sendMessage(message: string) {
showNotification(
"Message Sent",
`Message successfully sent to ${selectedCommunication.value.public_report?.reporter.name}`,
`Message successfully sent to ${selectedReport.value.reporter.name}`,
);
}
function showNotification(title: string, message: string) {
@ -263,7 +262,7 @@ function showNotification(title: string, message: string) {
function updateMap() {
let bounds = new Bounds();
const loc = selectedCommunication.value?.public_report?.location;
const loc = selectedReport.value?.location;
console.log("updating for loc", loc);
let markers: Marker[] = [];
if (loc && loc.latitude != 0 && loc.longitude != 0) {
@ -275,8 +274,7 @@ function updateMap() {
location: loc,
});
}
const address_loc =
selectedCommunication.value?.public_report?.address.location;
const address_loc = selectedReport.value?.address.location;
if (address_loc && address_loc.latitude != 0 && address_loc.longitude != 0) {
bounds.addLocation(address_loc);
markers.push({
@ -287,9 +285,7 @@ function updateMap() {
});
}
for (const [i, image] of (
selectedCommunication.value?.public_report?.images ?? []
).entries()) {
for (const [i, image] of (selectedReport.value?.images ?? []).entries()) {
if (
image.location != null &&
image.location.latitude != 0 &&
@ -309,25 +305,6 @@ function updateMap() {
}
// Lifecycle hooks
onMounted(async () => {
await loadFromAPI();
// Setup map layer after next tick to ensure map is mounted
/*
if (mapRef.value) {
const mapEl = mapRef.value.$el || mapRef.value;
mapEl.addEventListener("load", () => {
mapEl.addLayer({
id: "parcel",
minzoom: 14,
paint: {
"line-color": "#0f0",
},
source: "tegola",
"source-layer": "parcel",
type: "line",
});
});
}
*/
await storeCommunication.fetchAll();
});
</script>