2026-03-22 00:22:16 +00:00
< template >
< div class = "h-100" >
< div class = "container-fluid h-100" >
< div class = "row h-100" >
<!-- Left Column - Communications List -- >
2026-03-22 03:33:52 +00:00
< CommunicationColumnList :all = "communication.all" :loading = "loading" / >
2026-03-22 00:22:16 +00:00
<!-- Middle Column - Report Details -- >
< div class = "col-md-6 p-0" >
< div class = "p-3" >
< div class = "map-container" >
< MapMultipoint
id = "map"
ref = "mapRef"
2026-03-22 01:23:08 +00:00
: organization - id = "user.organization.id"
: tegola = "user.urls.tegola"
: xmin = "user.organization.service_area?.min.x ?? 0"
: ymin = "user.organization.service_area?.min.y ?? 0"
: xmax = "user.organization.service_area?.max.x ?? 0"
: ymax = "user.organization.service_area?.max.y ?? 0"
2026-03-22 00:22:16 +00:00
/ >
< / div >
< / 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" >
<!-- Report Details -- >
< div class = "details-section p-3 border-top" >
< div
class = "d-flex justify-content-between align-items-start mb-3"
>
< div >
< h5 class = "mb-1" >
< span
v - if = "
selectedCommunication . type === 'publicreport.nuisance'
"
>
< i class = "bi bi-mosquito icon-nuisance" > < / i >
Nuisance Report
< / span >
< span
v - if = "selectedCommunication.type === 'publicreport.water'"
>
< i class = "bi bi-droplet-fill icon-standing-water" > < / i >
Standing Water Report
< / span >
< / h5 >
< small class = "text-muted"
> Report ID : # { { selectedCommunication . id } } < / s m a l l
>
< / div >
< span class = "badge bg-secondary" >
< TimeRelative :time = "selectedCommunication.created" / >
< / span >
< / div >
<!-- Common Fields -- >
< div class = "card mb-3" >
< div class = "card-body" >
< div class = "row g-3" >
< div class = "col-12" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-geo-alt" > < / i > Address
< / label >
< div class = "fw-medium" >
{ {
formatAddress (
selectedCommunication . public _report . address ,
)
} }
< / div >
< / div >
< div class = "col-md-6" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-person" > < / i > Reporter Name
< / label >
< div class = "fw-medium" >
{ {
selectedCommunication . public _report . reporter . name ||
"not given"
} }
< / div >
< / div >
< div class = "col-md-6" >
< label
v - if = "
selectedCommunication . public _report . reporter . has _email
"
class = "form-label text-muted small mb-0"
>
< i class = "bi bi-envelope" > < / i >
< / label >
< label
v - if = "
selectedCommunication . public _report . reporter . has _phone
"
class = "form-label text-muted small mb-0"
>
< i class = "bi bi-phone" > < / i >
< / label >
< / div >
< / div >
< div v-if = "water" class="row g-3" >
< div class = "col-12" >
< ul >
< li v-if = "water?.is_reporter_owner" >
Reporter is the owner of the property
< / li >
< li v-if = "water?.is_reporter_confidential" >
Reporter has asked to be kept confidential
< / li >
< / ul >
< / div >
< / div >
< / div >
< / div >
<!-- Nuisance - specific Fields -- >
< div v-if = "nuisance" class="card mb-3" >
< div class = "card-header bg-danger bg-opacity-10" >
< i class = "bi bi-exclamation-triangle" > < / i > Nuisance Details
< / div >
< div class = "card-body" >
< div class = "row g-3" >
< div class = "col-md-6" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-clock" > < / i > Time of Day Encountered
< / label >
< ul >
< li v-if = "nuisance?.time_of_day_early" > Early < / li >
< li v-if = "nuisance?.time_of_day_day" > Daytime < / li >
< li v-if = "nuisance?.time_of_day_evening" > Evening < / li >
< li v-if = "nuisance?.time_of_day_night" > Night < / li >
< / ul >
< / div >
< div class = "col-md-6" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-house" > < / i > Property Area
< / label >
< div >
< ul >
< li v-if = "nuisance?.is_location_backyard" >
Backyard
< / li >
< li v-if = "nuisance?.is_location_frontyard" >
Frontyard
< / li >
< li v-if = "nuisance?.is_location_garden" > Garden < / li >
< li v-if = "nuisance?.is_location_other" > Other < / li >
< li v-if = "nuisance?.is_location_pool" > Pool < / li >
< / ul >
< / div >
< / div >
< div
v - if = "
nuisance ? . source _container ||
nuisance ? . source _gutter ||
nuisance ? . source _stagnant
"
class = "col-md-6"
>
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-droplet" > < / i > Sources
< / label >
< ul >
< li v-if = "nuisance?.source_container" > Container < / li >
< li v-if = "nuisance?.source_gutter" > Gutter < / li >
< li v-if = "nuisance?.source_stagnant" >
Sprinklers & Gutters
< / li >
< / ul >
< / div >
< div v-if = "nuisance?.source_description" class="col-12" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-chat-text" > < / i > Source Description
< / label >
< div class = "p-2 bg-light rounded" >
{ { nuisance ? . source _description || "none" } }
< / div >
< / div >
< div class = "col-12" >
< label class = "form-label text-mudet small mb-0" >
< i class = "bi bi-clock" > < / i > Duration
< / label >
< div class = "p-2 bg-light rounded" >
{ { nuisance ? . duration } }
< / div >
< / div >
< div class = "col-12" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-chat-text" > < / i > Additional Notes
< / label >
< div class = "p-2 bg-light rounded" >
{ { nuisance ? . additional _info || "No additional notes" } }
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Standing Water - specific Fields -- >
< div v-if = "water" class="card mb-3" >
< div class = "card-header bg-info bg-opacity-10" >
< i class = "bi bi-droplet" > < / i > Standing Water Details
< / div >
< div class = "card-body" >
< div
v - if = "
water ? . access _gate ||
water ? . access _fence ||
water ? . access _locked ||
water ? . access _dog ||
water ? . access _other
"
class = "col-md-6"
>
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-droplet" > < / i > Access
< / label >
< div >
< ul >
< li v-if = "water?.access_gate" > Gate < / li >
< li v-if = "water?.access_fence" > Fence < / li >
< li v-if = "water?.access_locked" > Locked < / li >
< li v-if = "water?.access_dog" > Dog < / li >
< li v-if = "water?.access_other" >
Other access obstacle
< / li >
< / ul >
< / div >
< / div >
< div v-if = "water?.access_comments" class="col-12" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-chat-text" > < / i > Access Comments
< / label >
< div class = "p-2 bg-light rounded" >
{ { water ? . access _comments } }
< / div >
< / div >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-eye" > < / i > Mosquito Life Stages Observed
< / label >
< div class = "mt-2" >
< span
class = "badge me-2"
: class = "
water ? . has _larvae
? 'badge-larvae'
: 'bg-light text-muted'
"
>
< i
class = "bi"
: class = "
water ? . has _larvae ? 'bi-check-circle' : 'bi-circle'
"
> < / i >
Larvae
< / span >
< span
class = "badge me-2"
: class = "
water ? . has _pupae ? 'badge-pupae' : 'bg-light text-muted'
"
>
< i
class = "bi"
: class = "
water ? . has _pupae ? 'bi-check-circle' : 'bi-circle'
"
> < / i >
Pupae
< / span >
< span
class = "badge"
: class = "
water ? . has _adult ? 'badge-adult' : 'bg-light text-muted'
"
>
< i
class = "bi"
: class = "
water ? . has _adult ? 'bi-check-circle' : 'bi-circle'
"
> < / i >
Adult Mosquitoes
< / span >
< / div >
< div v-if = "water?.comments" class="col-12" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-chat-text" > < / i > Comments
< / label >
< div class = "p-2 bg-light rounded" >
{ { water ? . comments } }
< / div >
< / div >
< div class = "col-md-6" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-person" > < / i > Owner Name
< / label >
< div class = "fw-medium" >
{ { water ? . owner . name || "not given" } }
< / div >
< / div >
< div class = "col-md-6" >
< label
v - if = "water?.owner.has_email"
class = "form-label text-muted small mb-0"
>
< i class = "bi bi-envelope" > < / i >
< / label >
< label
v - if = "water?.owner.has_phone"
class = "form-label text-muted small mb-0"
>
< i class = "bi bi-phone" > < / i >
< / label >
< / div >
< / div >
< / div >
<!-- Photos Section -- >
< div class = "card" >
< div
class = "card-header d-flex justify-content-between align-items-center"
>
< span > < i class = "bi bi-images" > < / i > Attached Photos < / span >
< span class = "badge bg-primary" >
{ {
selectedCommunication . public _report . images ? . length || 0
} }
< / span >
< / div >
< div class = "card-body" >
< div
v - if = "
selectedCommunication . public _report . images &&
selectedCommunication . public _report . images . length > 0
"
class = "d-flex flex-wrap gap-2"
>
< img
v - for = " ( photo , index ) in selectedCommunication
. public _report . images "
: key = "index"
: src = "photo.url_content"
class = "photo-thumbnail"
@ click = "openPhotoViewer(index)"
: alt = "'Photo ' + (index + 1)"
/ >
< / div >
< div
v - if = "
! selectedCommunication . public _report . images ||
selectedCommunication . public _report . images . length === 0
"
class = "text-muted text-center py-3"
>
< i class = "bi bi-camera-slash fs-4" > < / i >
< p class = "mb-0 small" > No images attached < / p >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Right Column - Actions -- >
< div class = "col-md-3 border-start p-0" >
< div
v - if = "!selectedCommunication"
class = "h-100 d-flex flex-column align-items-center justify-content-center text-muted p-3"
>
< i class = "bi bi-gear fs-1" > < / i >
< p class = "mt-2 text-center" >
Actions will appear here when a report is selected
< / p >
< / div >
< div
v - if = "selectedCommunication"
class = "actions-panel d-flex flex-column"
>
< div class = "p-3 bg-light border-bottom" >
< h6 class = "mb-0" >
< i class = "bi bi-lightning" > < / i > Quick Actions
< / h6 >
< / div >
< div class = "p-3 flex-grow-1" >
<!-- Create Signal -- >
< div class = "d-grid mb-3" >
< button class = "btn btn-success btn-lg" @click ="createSignal()" >
< i class = "bi bi-plus-circle me-2" > < / i > Mark Signal
< / button >
< small class = "text-muted mt-1"
> This report is useful signal < / s m a l l
>
< / div >
<!-- Mark Invalid -- >
< div class = "d-grid mb-3" >
< button class = "btn btn-outline-danger" @click ="markInvalid()" >
< i class = "bi bi-x-circle me-2" > < / i > Mark Invalid
< / button >
< small class = "text-muted mt-1" > This report isn ' t useful < / small >
< / div >
< hr / >
<!-- Message Reporter -- >
< div
v - if = "
! (
selectedCommunication ? . public _report . reporter . has _email ||
selectedCommunication ? . public _report . reporter . has _phone
)
"
class = "mb-3"
>
< h6 >
< i class = "bi bi-chat-dots" > < / i > No Reporter Communications
Available
< / h6 >
< / div >
< div
v - if = "
selectedCommunication ? . public _report . reporter . has _email ||
selectedCommunication ? . public _report . reporter . has _phone
"
class = "mb-3"
>
< h6 > < i class = "bi bi-chat-dots" > < / i > Message Reporter < / h6 >
< div class = "mb-2" >
< label class = "form-label small text-muted"
> Quick Templates < / l a b e l
>
< select
class = "form-select form-select-sm"
@ change = "applyMessageTemplate($event.target.value)"
>
< option value = "" > Select a template ... < / option >
< option value = "received" > Report Received < / option >
< option value = "scheduled" > Service Scheduled < / option >
< option value = "completed" > Service Completed < / option >
< option value = "need_info" > Need More Information < / option >
< / select >
< / div >
< textarea
class = "form-control mb-2"
rows = "5"
v - model = "messageText"
placeholder = "Type your message to the reporter..."
> < / textarea >
< div class = "d-grid" >
< button
class = "btn btn-primary"
@ click = "sendMessage()"
: disabled = "!messageText.trim()"
>
< i class = "bi bi-send me-2" > < / i > Send Message
< / button >
< / div >
< / div >
< hr / >
<!-- Report History -- >
< div >
< h6 > < i class = "bi bi-clock-history" > < / i > Activity Log < / h6 >
< div class = "small" >
< div
v - for = " entry in selectedCommunication . public _report . log ||
[ ] "
: key = "entry.created"
class = "border-start border-2 ps-2 mb-2"
>
< div v-if = "entry.type === 'created'" >
< div class = "text-muted" > Initial Report < / div >
< small class = "text-muted" > { {
formatDate ( entry . created )
} } < / small >
< / div >
< div v -else -if = " entry.type = = = ' message -text ' " >
< div class = "text-muted" > Text Message < / div >
< div > { { entry . message } } < / div >
< small class = "text-muted" > { {
formatDate ( entry . created )
} } < / small >
< / div >
< div v-else > {{ entry.type }} < / div >
< / div >
< div
v - if = "
! selectedCommunication . public _report . log ||
selectedCommunication . public _report . log . length === 0
"
class = "text-muted"
>
No activity yet
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Photo Viewer Modal -- >
< div
class = "modal fade"
: class = "{ 'show d-block': showPhotoModal }"
tabindex = "-1"
v - show = "showPhotoModal"
@ click . self = "showPhotoModal = false"
>
< div class = "modal-dialog modal-lg modal-dialog-centered" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" >
Photo { { currentPhotoIndex + 1 } } of
{ { selectedCommunication ? . public _report . images . length || 0 } }
< / h5 >
< button
type = "button"
class = "btn-close"
@ click = "showPhotoModal = false"
> < / button >
< / div >
< div class = "modal-body text-center" >
< div v-if = "selectedCommunication && showPhotoModal" >
< img
: src = "
selectedCommunication . public _report . images [ currentPhotoIndex ]
. url _content
"
class = "img-fluid rounded"
style = "max-height: 60vh"
/ >
<!-- EXIF Data Section -- >
< div class = "mt-4 pt-3 border-top text-start" >
< h6 class = "text-muted mb-3" > Photo Information < / h6 >
< div class = "row g-3" >
< div class = "col-md-4" >
< small class = "text-muted d-block" > Date Taken < / small >
< span >
{ {
selectedCommunication . public _report . images [
currentPhotoIndex
] . exif ? . created || "N/A"
} }
< / span >
< / div >
< div class = "col-md-4" >
< small class = "text-muted d-block" > Camera < / small >
< span >
{ {
( selectedCommunication . public _report . images [
currentPhotoIndex
] . exif ? . make || "" ) +
" " +
( selectedCommunication . public _report . images [
currentPhotoIndex
] . exif ? . model || "" ) || "N/A"
} }
< / span >
< / div >
< div class = "col-md-4" >
< small class = "text-muted d-block"
> Distance from Reporter < / s m a l l
>
< span
v - if = "
selectedCommunication . public _report . images [
currentPhotoIndex
] . location != null
"
>
{ {
formatDistance (
selectedCommunication . public _report . images [
currentPhotoIndex
] . distance _from _reporter _meters ,
)
} }
< / span >
< span v-else > No location data in image < / span >
< / div >
< / div >
< / div >
< / div >
< / div >
< div class = "modal-footer justify-content-between" >
< button
class = "btn btn-outline-secondary"
@ click = "currentPhotoIndex = Math.max(0, currentPhotoIndex - 1)"
: disabled = "currentPhotoIndex === 0"
>
< i class = "bi bi-chevron-left" > < / i > Previous
< / button >
< button
class = "btn btn-outline-secondary"
@ click = "
currentPhotoIndex = Math . min (
selectedCommunication . public _report . images . length - 1 ,
currentPhotoIndex + 1 ,
)
"
: disabled = "
currentPhotoIndex >=
( selectedCommunication ? . public _report . images ? . length || 1 ) - 1
"
>
Next < i class = "bi bi-chevron-right" > < / i >
< / button >
< / div >
< / div >
< / div >
< / div >
< div
class = "modal-backdrop fade show"
v - show = "showPhotoModal"
@ click = "showPhotoModal = false"
> < / div >
<!-- Toast Notifications -- >
< div class = "toast-container position-fixed bottom-0 end-0 p-3" >
< div class = "toast" : class = "{ show: showToast }" role = "alert" >
< div class = "toast-header" >
< i class = "bi bi-check-circle text-success me-2" > < / i >
< strong class = "me-auto" > { { toastTitle } } < / strong >
< button
type = "button"
class = "btn-close"
@ click = "showToast = false"
> < / button >
< / div >
< div class = "toast-body" > { { toastMessage } } < / div >
< / div >
< / div >
< / div >
< / template >
2026-03-22 03:33:52 +00:00
< script setup lang = "ts" >
2026-03-22 00:22:16 +00:00
import { ref , computed , onMounted , nextTick } from "vue" ;
import maplibregl from "maplibre-gl" ;
2026-03-22 00:55:48 +00:00
import { useCommunicationStore } from "../store/communication" ;
import { useUserStore } from "../store/user" ;
2026-03-22 03:01:49 +00:00
import CommunicationColumnList from "../components/CommunicationColumnList.vue" ;
2026-03-22 00:22:16 +00:00
import MapMultipoint from "../components/MapMultipoint.vue" ;
import TimeRelative from "../components/TimeRelative.vue" ;
2026-03-22 02:37:10 +00:00
const communication = useCommunicationStore ( ) ;
2026-03-22 00:55:48 +00:00
const user = useUserStore ( ) ;
onMounted ( ( ) => {
2026-03-22 03:01:49 +00:00
fetchCommunications ( ) ;
2026-03-22 00:55:48 +00:00
} ) ;
2026-03-22 00:22:16 +00:00
// Refs
const apiBase = ref ( "/api" ) ;
const selectedCommunication = ref ( null ) ;
const messageText = ref ( "" ) ;
const showPhotoModal = ref ( false ) ;
const currentPhotoIndex = ref ( 0 ) ;
const showToast = ref ( false ) ;
const toastTitle = ref ( "" ) ;
const toastMessage = ref ( "" ) ;
2026-03-22 03:33:52 +00:00
const loading = ref ( true ) ;
2026-03-22 00:22:16 +00:00
const error = ref ( null ) ;
const mapRef = ref ( null ) ;
const nuisance = computed ( ( ) => {
return selectedCommunication . value ? . public _report ? . nuisance || null ;
} ) ;
const water = computed ( ( ) => {
return selectedCommunication . value ? . public _report ? . water || null ;
} ) ;
2026-03-22 02:37:10 +00:00
async function fetchCommunications ( ) {
await communication . fetchAll ( ) ;
// if we already had something selected, reset it using the new data
if ( selectedCommunication . value ) {
const matching = communication . all . filter ( ( c ) => {
return c . id === selectedCommunication . value . id ;
} ) ;
if ( matching . length > 0 ) {
selectedCommunication . value = matching [ 0 ] ;
}
}
}
2026-03-22 00:22:16 +00:00
// Methods
function filterMatches ( filter , comm ) {
// Implement your filter logic here
return true ;
}
function formatAddress ( a ) {
if ( a . number === "" && a . street === "" ) {
return "no address provided" ;
}
return ` $ ${ a . number } $ ${ a . street } , ${ a . locality } ` ;
}
function formatDistance ( meters ) {
if ( meters === undefined || meters === null ) {
return "unknown" ;
}
if ( meters < 1 ) {
const mm = Math . round ( meters * 1000 ) ;
return ` ${ mm } mm ` ;
} else if ( meters >= 1000 ) {
const km = Math . round ( meters / 1000 ) ;
return ` ${ km } km ` ;
} else {
const m = Math . round ( meters ) ;
return ` ${ m } m ` ;
}
}
function formatDate ( date ) {
return new Date ( date ) . toLocaleString ( ) ;
}
async function loadFromAPI ( ) {
loading . value = true ;
error . value = null ;
try {
await Promise . all ( [ fetchCommunications ( ) ] ) ;
} catch ( err ) {
error . value = err . message ;
console . error ( "Error loading data:" , err ) ;
} finally {
loading . value = false ;
}
}
function selectCommunication ( comm ) {
selectedCommunication . value = comm ;
messageText . value = "" ;
updateMap ( ) ;
}
function openPhotoViewer ( index ) {
currentPhotoIndex . value = index ;
showPhotoModal . value = true ;
}
function applyMessageTemplate ( template ) {
const templates = {
received : ` Dear ${ selectedCommunication . value ? . 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 ${ selectedCommunication . value ? . 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 ${ selectedCommunication . value ? . 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 ${ selectedCommunication . value ? . 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 ` ,
} ;
if ( templates [ template ] ) {
messageText . value = templates [ template ] ;
}
}
async function createSignal ( ) {
console . log ( "Marking report as signal:" , selectedCommunication . value . id ) ;
try {
const report _id = selectedCommunication . value . id ;
const payload = {
reportID : report _id ,
} ;
removeCurrentFromList ( ) ;
const response = await fetch ( "api/publicreport/signal" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
} ,
body : JSON . stringify ( payload ) ,
} ) ;
if ( ! response . ok ) {
throw new Error ( "Failed to submit signal" ) ;
}
showNotification (
"Report Marked Signal" ,
` Report # ${ report _id } has been marked as useful signal ` ,
) ;
await fetchCommunications ( ) ;
} catch ( err ) {
error . value = err . message ;
console . error ( "Error creating lead:" , err ) ;
}
}
async function markInvalid ( ) {
console . log ( "Marking report as invalid:" , selectedCommunication . value . id ) ;
const payload = {
reportID : selectedCommunication . value . id ,
} ;
const response = await fetch ( "api/publicreport/invalid" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
} ,
body : JSON . stringify ( payload ) ,
} ) ;
showNotification (
"Report Marked Invalid" ,
` Report # ${ selectedCommunication . value . id } has been marked as invalid ` ,
) ;
removeCurrentFromList ( ) ;
await fetchCommunications ( ) ;
}
function removeCurrentFromList ( ) {
const index = communications . value . findIndex (
( c ) => c . id === selectedCommunication . value . id ,
) ;
if ( index > - 1 ) {
communications . value . splice ( index , 1 ) ;
}
if ( communications . value . length > 0 ) {
const nextIndex = Math . min ( index , communications . value . length - 1 ) ;
selectedCommunication . value = communications . value [ nextIndex ] ;
} else {
selectedCommunication . value = null ;
}
}
async function sendMessage ( ) {
if ( ! messageText . value . trim ( ) ) return ;
console . log ( "Sending message reporter:" , messageText . value ) ;
const payload = {
message : messageText . value ,
reportID : selectedCommunication . value . id ,
} ;
const response = await fetch ( ` ${ apiBase . value } /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 ${ selectedCommunication . value . public _report . reporter . name } ` ,
) ;
messageText . value = "" ;
}
function showNotification ( title , message ) {
toastTitle . value = title ;
toastMessage . value = message ;
showToast . value = true ;
setTimeout ( ( ) => {
showToast . value = false ;
} , 3000 ) ;
}
function updateMap ( ) {
if ( ! mapRef . value ) return ;
const map = mapRef . value . $el || mapRef . value ;
const loc = selectedCommunication . value . public _report . location ;
if ( loc == null ) {
map . ClearMarkers ( ) ;
map . ResetCamera ( ) ;
return ;
}
let markers = [
new maplibregl . Marker ( {
color : "#FF0000" ,
draggable : false ,
} ) . setLngLat ( [ loc . longitude , loc . latitude ] ) ,
] ;
let min = { lat : loc . latitude , lng : loc . longitude } ;
let max = { lat : loc . latitude , lng : loc . longitude } ;
for ( const i of selectedCommunication . value . public _report . images ) {
if (
i . location != null &&
i . location . latitude != 0 &&
i . location . longitude != 0
) {
markers . push (
new maplibregl . Marker ( {
color : "#00FF00" ,
draggable : false ,
} ) . setLngLat ( [ i . location . longitude , i . location . latitude ] ) ,
) ;
min . lat = Math . min ( min . lat , i . location . latitude ) ;
min . lng = Math . min ( min . lng , i . location . longitude ) ;
max . lat = Math . max ( max . lat , i . location . latitude ) ;
max . lng = Math . max ( max . lng , i . location . longitude ) ;
}
}
map . SetMarkers ( markers ) ;
const bounds = new maplibregl . LngLatBounds (
new maplibregl . LngLat ( min . lng - 0.01 , min . lat - 0.01 ) ,
new maplibregl . LngLat ( max . lng + 0.01 , max . lat + 0.01 ) ,
) ;
map . FitBounds ( bounds , {
padding : 50 ,
} ) ;
}
2026-03-22 03:33:52 +00:00
function onFilterChange ( filters ) {
console . log ( "Filters changed" ) ;
}
2026-03-22 00:22:16 +00:00
// Lifecycle hooks
onMounted ( async ( ) => {
await loadFromAPI ( ) ;
// Subscribe to SSE events
if ( window . SSEManager ) {
window . SSEManager . subscribe ( "*" , ( e ) => {
if ( e . resource . startsWith ( "rmo:" ) ) {
fetchCommunications ( ) ;
}
} ) ;
}
// Setup map layer after next tick to ensure map is mounted
await nextTick ( ) ;
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" ,
} ) ;
} ) ;
}
} ) ;
< / script >
< style scoped >
/* Add your component-specific styles here */
. reports - list {
overflow - y : auto ;
max - height : 100 vh ;
}
. report - card {
cursor : pointer ;
transition : background - color 0.2 s ;
}
. report - card : hover {
background - color : # f8f9fa ;
}
. report - card . active {
background - color : # 0 d6efd ;
color : white ;
}
. map - container {
height : 400 px ;
width : 100 % ;
}
. photo - thumbnail {
width : 100 px ;
height : 100 px ;
object - fit : cover ;
cursor : pointer ;
border - radius : 4 px ;
transition : transform 0.2 s ;
}
. photo - thumbnail : hover {
transform : scale ( 1.05 ) ;
}
. details - section {
overflow - y : auto ;
}
. actions - panel {
height : 100 % ;
overflow - y : auto ;
}
. badge - larvae {
background - color : # ffc107 ;
color : # 000 ;
}
. badge - pupae {
background - color : # fd7e14 ;
color : # fff ;
}
. badge - adult {
background - color : # dc3545 ;
color : # fff ;
}
. icon - standing - water {
color : # 0 dcaf0 ;
}
. icon - nuisance {
color : # dc3545 ;
}
. modal . show {
background - color : rgba ( 0 , 0 , 0 , 0.5 ) ;
}
< / style >