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:
parent
6e3d079c46
commit
b68d93ec91
12 changed files with 184 additions and 233 deletions
|
|
@ -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))
|
||||
|
|
|
|||
9
main.go
9
main.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue