2026-02-27 16:51:41 +00:00
{{ template "sync/layout/authenticated.html" . }}
{{ define "title" }}Planning{{ end }}
{{ define "extraheader" }}
< script
type="text/javascript"
src="//unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js"
>< / script >
2026-03-07 01:20:47 +00:00
< script src = "/static/js/time-relative.js" > < / script >
2026-03-07 01:38:05 +00:00
< script src = "/static/js/map-multipoint.js" > < / script >
2026-02-27 16:51:41 +00:00
< script >
2026-03-06 23:45:12 +00:00
function filterMatches(filter, comm) {
return true;
2026-02-27 16:51:41 +00:00
}
2026-03-06 23:45:12 +00:00
function formatAddress(a) {
2026-03-09 23:02:11 +00:00
if (a.number == "" & & a.street == "") {
return "no address provided";
}
2026-03-09 23:26:24 +00:00
return a.number + " " + a.street + ", " + a.locality;
2026-02-27 16:51:41 +00:00
}
2026-03-10 04:58:43 +00:00
function formatDistance(meters) {
if (meters == undefined || meters == null) {
return "unknown";
}
if (meters < 1 ) {
// Convert to millimeters
const mm = Math.round(meters * 1000);
return `${mm} mm`;
} else if (meters >= 1000) {
// Convert to kilometers
const km = Math.round(meters / 1000);
return `${km} km`;
} else {
// Keep in meters
const m = Math.round(meters);
return `${m} m`;
}
}
2026-03-06 22:07:48 +00:00
function communicationsApp() {
return {
2026-03-06 23:45:12 +00:00
apiBase: "/api",
2026-03-06 22:07:48 +00:00
// State
2026-03-06 23:45:12 +00:00
selectedCommunication: null,
2026-03-06 22:07:48 +00:00
searchFilter: "",
typeFilter: "all",
messageText: "",
showPhotoModal: false,
currentPhotoIndex: 0,
showToast: false,
toastTitle: "",
toastMessage: "",
2026-03-10 16:20:28 +00:00
communications: [],
2026-03-06 22:07:48 +00:00
2026-03-06 23:45:12 +00:00
init() {
this.loadFromAPI();
2026-03-07 01:38:05 +00:00
document.addEventListener("DOMContentLoaded", () => {
2026-03-14 18:13:51 +00:00
SSEManager.subscribe("*", (e) => {
if (e.resource.startsWith("rmo:")) {
this.fetchCommunications();
}
});
2026-03-07 01:38:05 +00:00
const map = document.querySelector("map-multipoint");
map.on("load", () => {
map.addLayer({
id: "parcel",
minzoom: 14,
paint: {
"line-color": "#0f0",
},
source: "tegola",
"source-layer": "parcel",
type: "line",
});
});
});
2026-03-06 23:45:12 +00:00
},
2026-03-06 22:07:48 +00:00
// Computed property for filtered reports
2026-03-06 23:45:12 +00:00
get filteredCommunications() {
return this.communications.filter((report) => {
2026-03-06 22:07:48 +00:00
const matchesType =
this.typeFilter === "all" || report.type === this.typeFilter;
2026-03-06 23:45:12 +00:00
return matchesType & & filterMatches(this.searchFilter, report);
2026-03-06 22:07:48 +00:00
});
},
2026-03-14 01:14:30 +00:00
async fetchCommunications() {
2026-03-06 23:45:12 +00:00
try {
// Build query parameters from filters
const params = new URLSearchParams();
2026-03-10 15:46:17 +00:00
params.append("sort", "-created");
2026-03-06 23:45:12 +00:00
if (this.typeFilter) params.append("type", this.typeFilter);
const response = await fetch(
`${this.apiBase}/communication?${params}`,
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
2026-03-20 18:29:29 +00:00
this.communications = data.communications;
2026-03-14 20:03:46 +00:00
// if we already had something selected, reset it using the new data
if (this.selectedCommunication) {
const matching = this.communications.filter((report) => {
return report.id == this.selectedCommunication.id;
});
if (matching.length > 0) {
this.selectedCommunication = matching[0];
}
}
2026-03-06 23:45:12 +00:00
} catch (err) {
console.error("Error loading communications:", err);
throw err;
}
},
async loadFromAPI() {
this.loading = true;
this.error = null;
try {
2026-03-14 01:14:30 +00:00
await Promise.all([this.fetchCommunications()]);
2026-03-06 23:45:12 +00:00
} catch (err) {
this.error = err.message;
console.error("Error loading data:", err);
}
},
2026-03-20 18:52:21 +00:00
nuisance() {
return this.selectedCommunication?.public_report?.nuisance;
},
2026-03-06 22:07:48 +00:00
// Methods
2026-03-20 18:30:02 +00:00
selectCommunication(comm) {
this.selectedCommunication = comm;
2026-03-06 22:07:48 +00:00
this.messageText = "";
2026-03-07 01:52:59 +00:00
this.updateMap();
2026-03-06 22:07:48 +00:00
},
formatDate(date) {
return new Date(date).toLocaleString();
},
openPhotoViewer(index) {
this.currentPhotoIndex = index;
this.showPhotoModal = true;
},
applyMessageTemplate(template) {
const templates = {
2026-03-07 01:20:47 +00:00
received: `Dear ${this.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 ${this.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 ${this.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 ${this.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`,
2026-03-06 22:07:48 +00:00
};
if (templates[template]) {
this.messageText = templates[template];
}
},
2026-03-19 17:41:38 +00:00
async createSignal() {
2026-03-19 19:07:48 +00:00
console.log(
"Marking report as signal:",
this.selectedCommunication.id,
);
2026-03-14 01:14:30 +00:00
try {
2026-03-19 19:32:06 +00:00
const report_id = this.selectedCommunication.id;
2026-03-14 01:14:30 +00:00
const payload = {
2026-03-19 19:32:06 +00:00
reportID: report_id,
2026-03-14 01:14:30 +00:00
};
2026-03-19 19:32:06 +00:00
this.removeCurrentFromList();
2026-03-19 17:41:38 +00:00
const response = await fetch(`api/publicreport/signal`, {
2026-03-14 01:14:30 +00:00
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
2026-03-19 17:41:38 +00:00
throw new Error("Failed to submit signal");
2026-03-14 01:14:30 +00:00
}
2026-03-19 19:07:48 +00:00
this.showNotification(
"Report Marked Signal",
2026-03-19 19:32:06 +00:00
`Report #${report_id} has been marked as useful signal`,
2026-03-19 19:07:48 +00:00
);
2026-03-14 01:14:30 +00:00
this.fetchCommunications();
} catch (err) {
this.error = err.message;
console.error("Error creating lead:", err);
2026-03-06 22:07:48 +00:00
}
},
2026-03-14 01:14:30 +00:00
async markInvalid() {
2026-03-06 22:07:48 +00:00
console.log(
"Marking report as invalid:",
2026-03-06 23:45:12 +00:00
this.selectedCommunication.id,
2026-03-06 22:07:48 +00:00
);
2026-03-14 01:14:30 +00:00
const payload = {
reportID: this.selectedCommunication.id,
};
const response = await fetch(`api/publicreport/invalid`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
2026-03-06 22:07:48 +00:00
this.showNotification(
"Report Marked Invalid",
2026-03-14 01:14:30 +00:00
`Report #${this.selectedCommunication.id} has been marked as invalid`,
2026-03-06 22:07:48 +00:00
);
2026-03-14 01:14:30 +00:00
this.removeCurrentFromList();
this.fetchCommunications();
},
removeCurrentFromList() {
const index = this.communications.findIndex(
(c) => c.id === this.selectedCommunication.id,
2026-03-06 22:07:48 +00:00
);
2026-03-14 01:14:30 +00:00
if (index > -1) {
2026-03-19 19:32:06 +00:00
this.communications.splice(index, 1);
2026-03-14 01:14:30 +00:00
}
if (this.communications.length > 0) {
const nextIndex = Math.min(index, this.communications.length - 1);
this.selectedCommunication = this.communications[nextIndex];
} else {
this.selectedCommunication = null;
}
2026-03-06 22:07:48 +00:00
},
2026-03-15 22:38:36 +00:00
async sendMessage() {
2026-03-06 22:07:48 +00:00
if (!this.messageText.trim()) return;
2026-03-15 22:38:36 +00:00
console.log("Sending message reporter:", this.messageText);
const payload = {
message: this.messageText,
reportID: this.selectedCommunication.id,
};
const response = await fetch(`${this.apiBase}/publicreport/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
2026-03-06 22:07:48 +00:00
this.showNotification(
"Message Sent",
2026-03-07 01:20:47 +00:00
`Message successfully sent to ${this.selectedCommunication.public_report.reporter.name}`,
2026-03-06 22:07:48 +00:00
);
this.messageText = "";
},
showNotification(title, message) {
this.toastTitle = title;
this.toastMessage = message;
this.showToast = true;
setTimeout(() => {
this.showToast = false;
}, 3000);
},
// Initialize map (call this on page load if using a map library)
initMap() {
// TODO: Initialize your map library here
// Example with Leaflet:
// this.map = L.map('map-container').setView([28.5383, -81.3792], 12);
// L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(this.map);
console.log(
"Map initialization placeholder - implement with your preferred map library",
);
},
2026-03-07 01:52:59 +00:00
updateMap() {
const map = document.querySelector("map-multipoint");
2026-03-10 04:20:55 +00:00
const loc = this.selectedCommunication.public_report.location;
2026-03-19 19:16:39 +00:00
if (loc == null) {
map.ClearMarkers();
map.ResetCamera();
return;
}
2026-03-10 04:20:55 +00:00
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 this.selectedCommunication.public_report.images) {
2026-03-20 15:45:55 +00:00
if (
i.location != null & &
i.location.latitude != 0 & &
i.location.longitude != 0
) {
2026-03-10 04:20:55 +00:00
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);
2026-03-07 01:52:59 +00:00
const bounds = new maplibregl.LngLatBounds(
2026-03-10 04:20:55 +00:00
new maplibregl.LngLat(min.lng - 0.01, min.lat - 0.01),
new maplibregl.LngLat(max.lng + 0.01, max.lat + 0.01),
2026-03-07 01:52:59 +00:00
);
map.FitBounds(bounds, {
padding: 50,
});
},
2026-03-20 18:52:21 +00:00
water() {
return this.selectedCommunication?.public_report?.water;
},
2026-03-06 22:07:48 +00:00
};
}
< / script >
2026-02-27 16:51:41 +00:00
{{ end }}
{{ define "content" }}
2026-03-06 22:07:48 +00:00
< div x-data = "communicationsApp()" class = "h-100" >
< div class = "container-fluid h-100" >
< div class = "row h-100" >
2026-03-06 23:45:12 +00:00
<!-- Left Column - Communications List -->
2026-03-06 22:07:48 +00:00
< div class = "col-md-3 border-end p-0 reports-list" >
< div class = "p-3 bg-light border-bottom" >
< div class = "input-group input-group-sm" >
< span class = "input-group-text" > < i class = "bi bi-search" > < / i > < / span >
< input
type="text"
class="form-control"
placeholder="Filter reports..."
x-model="searchFilter"
/>
< / div >
< div class = "mt-2 d-flex gap-2" >
< button
class="btn btn-sm"
:class="typeFilter === 'all' ? 'btn-primary' : 'btn-outline-secondary'"
@click="typeFilter = 'all'"
>
All
< / button >
< button
class="btn btn-sm"
:class="typeFilter === 'nuisance' ? 'btn-danger' : 'btn-outline-secondary'"
@click="typeFilter = 'nuisance'"
>
2026-03-09 18:04:28 +00:00
< i class = "bi" > {{ template "mosquito.svg" }}< / i > Nuisance
2026-03-06 22:07:48 +00:00
< / button >
< button
class="btn btn-sm"
2026-03-10 15:47:30 +00:00
:class="typeFilter === 'water' ? 'btn-info' : 'btn-outline-secondary'"
@click="typeFilter = 'water'"
2026-03-06 22:07:48 +00:00
>
< i class = "bi bi-droplet" > < / i > Water
< / button >
< / div >
< / div >
< div class = "list-group list-group-flush" >
2026-03-06 23:45:12 +00:00
< template x-for = "comm in filteredCommunications" :key = "comm.id" >
2026-03-06 22:07:48 +00:00
< div
class="list-group-item report-card p-3"
2026-03-06 23:45:12 +00:00
:class="{ 'active': selectedCommunication & & selectedCommunication.id === comm.id }"
2026-03-07 01:52:59 +00:00
@click="selectCommunication(comm)"
2026-03-06 22:07:48 +00:00
>
2026-03-06 22:44:11 +00:00
<!-- 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" >
2026-03-18 19:26:49 +00:00
< template x-if = "comm.type === 'publicreport.nuisance'" >
2026-03-09 18:04:28 +00:00
< i class = "bi fs-4 me-2" > {{ template "mosquito.svg" }}< / i >
2026-03-06 22:07:48 +00:00
< / template >
2026-03-18 19:26:49 +00:00
< template x-if = "comm.type === 'publicreport.water'" >
2026-03-06 22:07:48 +00:00
< i
2026-03-06 22:44:11 +00:00
class="bi bi-droplet-fill icon-standing-water fs-4 me-2"
2026-03-06 22:07:48 +00:00
>< / i >
< / template >
2026-03-06 22:44:11 +00:00
< span
class="badge"
2026-03-18 19:26:49 +00:00
:class="comm.type === 'publicreport.nuisance' ? 'bg-danger' : 'bg-info'"
x-text="comm.type === 'publicreport.nuisance' ? 'Nuisance' : 'Standing Water'"
2026-03-06 22:44:11 +00:00
>< / span >
2026-03-06 22:07:48 +00:00
< / div >
2026-03-07 01:26:25 +00:00
< small
2026-03-07 01:20:47 +00:00
>< time-relative :time = "comm.created" > < / time-relative >
< / small >
2026-03-06 22:44:11 +00:00
< / div >
<!-- Details section: full width -->
< div >
< div >
< i class = "bi bi-geo-alt text-muted" > < / i >
2026-03-06 23:45:12 +00:00
< span
x-text="comm.public_report.address.postal_code"
class="fw-medium"
>< / span >
2026-03-06 22:44:11 +00:00
< / div >
< small
2026-03-09 23:02:11 +00:00
x-text="formatAddress(comm.public_report.address)"
2026-03-06 22:44:11 +00:00
>< / small >
2026-03-07 01:57:18 +00:00
< template
x-if="comm.public_report.images & & comm.public_report.images.length > 0"
>
2026-03-06 22:07:48 +00:00
< div class = "mt-1" >
2026-03-06 22:44:11 +00:00
< small class = "text-muted" >
< i class = "bi bi-camera" > < / i >
2026-03-07 01:57:18 +00:00
< span x-text = "comm.public_report.images.length" > < / span >
photo(s)
2026-03-06 22:44:11 +00:00
< / small >
2026-03-06 22:07:48 +00:00
< / div >
2026-03-06 22:44:11 +00:00
< / template >
2026-03-06 22:07:48 +00:00
< / div >
< / div >
< / template >
< / div >
2026-02-27 16:51:41 +00:00
2026-03-06 23:45:12 +00:00
< template x-if = "filteredCommunications.length === 0" >
2026-03-06 22:07:48 +00:00
< div class = "text-center text-muted p-4" >
< i class = "bi bi-inbox fs-1" > < / i >
< p class = "mt-2" > No reports found< / p >
< / div >
< / template >
2026-02-27 16:51:41 +00:00
< / div >
2026-03-06 22:07:48 +00:00
<!-- Middle Column - Report Details -->
< div class = "col-md-6 p-0" >
2026-03-07 01:38:05 +00:00
< div class = "p-3" >
< div class = "map-container" >
< map-multipoint
id="map"
organization-id="{{ .Organization.ID }}"
tegola="{{ .URL.Tegola }}"
2026-03-13 00:03:36 +00:00
xmin="{{ .Organization.ServiceArea.Min.X }}"
ymin="{{ .Organization.ServiceArea.Min.Y }}"
xmax="{{ .Organization.ServiceArea.Max.X }}"
ymax="{{ .Organization.ServiceArea.Max.Y }}"
2026-03-07 01:38:05 +00:00
>< / map-multipoint >
< / div >
< / div >
2026-03-06 23:45:12 +00:00
< template x-if = "!selectedCommunication" >
2026-03-06 22:07:48 +00:00
< div
2026-03-09 18:04:28 +00:00
class="d-flex flex-column align-items-center justify-content-center text-muted"
2026-03-06 22:07:48 +00:00
>
< i class = "bi bi-hand-index fs-1" > < / i >
< p class = "mt-2" > Select a report to view details< / p >
< / div >
< / template >
2026-02-27 16:51:41 +00:00
2026-03-06 23:45:12 +00:00
< template x-if = "selectedCommunication" >
2026-03-06 22:07:48 +00:00
< div 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" >
2026-03-06 23:45:12 +00:00
< template
2026-03-18 19:26:49 +00:00
x-if="selectedCommunication.type === 'publicreport.nuisance'"
2026-03-06 23:45:12 +00:00
>
2026-03-06 22:07:48 +00:00
< span
2026-03-09 18:04:28 +00:00
>< i class = "bi icon-nuisance"
>{{ template "mosquito.svg" }}< /i
>
2026-03-06 22:07:48 +00:00
Nuisance Report< /span
>
< / template >
2026-03-06 23:45:12 +00:00
< template
2026-03-18 19:26:49 +00:00
x-if="selectedCommunication.type === 'publicreport.water'"
2026-03-06 23:45:12 +00:00
>
2026-03-06 22:07:48 +00:00
< span
>< i
class="bi bi-droplet-fill icon-standing-water"
>< / i >
Standing Water Report< /span
>
< / template >
< / h5 >
< small class = "text-muted"
2026-03-06 23:45:12 +00:00
>Report ID: #< span
x-text="selectedCommunication.id"
>< /span
2026-03-06 22:07:48 +00:00
>< / small >
< / div >
2026-03-07 01:20:47 +00:00
< span class = "badge bg-secondary"
>< time-relative
:time="selectedCommunication.created"
>< /time-relative
2026-03-06 22:07:48 +00:00
>< / span >
< / div >
2026-02-27 16:51:41 +00:00
2026-03-06 22:07:48 +00:00
<!-- 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"
2026-03-06 23:45:12 +00:00
x-text="formatAddress(selectedCommunication.public_report.address)"
2026-03-06 22:07:48 +00:00
>< / 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"
2026-03-09 22:17:56 +00:00
x-text="selectedCommunication.public_report.reporter.name || 'not given'"
2026-03-06 22:07:48 +00:00
>< / div >
2026-03-09 19:12:07 +00:00
< / div >
< div class = "col-md-6" >
2026-03-09 18:04:28 +00:00
< template
x-if="selectedCommunication.public_report.reporter.has_email"
>
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-envelope" > < / i >
< / label >
< / template >
< template
x-if="selectedCommunication.public_report.reporter.has_phone"
>
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-phone" > < / i >
< / label >
< / template >
2026-03-06 22:07:48 +00:00
< / div >
< / div >
2026-03-20 18:52:21 +00:00
< template x-if = "water() != null" >
2026-03-20 18:38:57 +00:00
< div class = "row g-3" >
< div class = "col-12" >
< ul >
2026-03-20 18:52:21 +00:00
< template x-if = "water()?.is_reporter_owner" >
2026-03-20 18:38:57 +00:00
< li > Reporter is the owner of the property< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "water()?.is_reporter_confidential" >
2026-03-20 18:38:57 +00:00
< li >
Reporter has asked to be kept confidential
< / li >
< / template >
< / ul >
< / div >
< / div >
< / template >
2026-03-06 22:07:48 +00:00
< / div >
< / div >
2026-02-27 16:51:41 +00:00
2026-03-06 22:07:48 +00:00
<!-- Nuisance - specific Fields -->
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance() != null" >
2026-03-06 22:07:48 +00:00
< div 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 >
2026-03-07 02:18:40 +00:00
< ul >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.time_of_day_early" >
2026-03-07 02:18:40 +00:00
< li > Early< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.time_of_day_day" >
2026-03-07 02:18:40 +00:00
< li > Daytime< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.time_of_day_evening" >
2026-03-07 02:18:40 +00:00
< li > Evening< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.time_of_day_night" >
2026-03-07 02:18:40 +00:00
< li > Night< / li >
< / template >
< / ul >
2026-03-06 22:07:48 +00:00
< / 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 >
2026-03-07 02:18:40 +00:00
< ul >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.is_location_backyard" >
2026-03-07 02:18:40 +00:00
< li > Backyard< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template
x-if="nuisance()?.is_location_frontyard"
>
2026-03-07 02:18:40 +00:00
< li > Frontyard< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.is_location_garden" >
2026-03-07 02:18:40 +00:00
< li > Garden< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.is_location_other" >
2026-03-07 02:18:40 +00:00
< li > Other< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.is_location_pool" >
2026-03-07 02:18:40 +00:00
< li > Pool< / li >
< / template >
< / ul >
2026-03-06 22:07:48 +00:00
< / div >
< / div >
2026-03-20 18:09:27 +00:00
< template
2026-03-20 18:52:21 +00:00
x-if="nuisance()?.source_container || nuisance()?.source_gutter || nuisance()?.source_stagnant"
2026-03-20 18:09:27 +00:00
>
< div class = "col-md-6" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-droplet" > < / i > Sources
< / label >
< ul >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.source_container" >
2026-03-20 18:09:27 +00:00
< li > Container< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.source_gutter" >
2026-03-20 18:09:27 +00:00
< li > Gutter< / li >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.source_stagnant" >
2026-03-20 18:09:27 +00:00
< li > Sprinklers & Gutters< / li >
< / template >
< / ul >
< / div >
< / template >
2026-03-20 18:52:21 +00:00
< template x-if = "nuisance()?.source_description != ''" >
2026-03-09 19:23:16 +00:00
< div 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"
2026-03-20 18:52:21 +00:00
x-text="nuisance()?.source_description || 'none'"
2026-03-09 19:23:16 +00:00
>< / div >
< / div >
< / template >
2026-03-20 18:01:52 +00:00
< 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"
2026-03-20 18:52:21 +00:00
x-text="nuisance()?.duration"
2026-03-20 18:01:52 +00:00
>< / div >
< / div >
2026-03-06 22:07:48 +00:00
< 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"
2026-03-20 18:52:21 +00:00
x-text="nuisance()?.additional_info || 'No additional notes'"
2026-03-06 22:07:48 +00:00
>< / div >
< / div >
< / div >
< / div >
< / div >
< / template >
2026-02-27 16:51:41 +00:00
2026-03-06 22:07:48 +00:00
<!-- Standing Water - specific Fields -->
2026-03-20 18:54:37 +00:00
< template x-if = "water() != null" >
2026-03-06 22:07:48 +00:00
< div 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" >
2026-03-20 18:54:37 +00:00
< template
x-if="water()?.access_gate || water()?.access_fence || water()?.access_locked || water()?.access_dog || water()?.access_other"
>
< div class = "col-md-6" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-droplet" > < / i > Access
< / label >
< div >
< ul >
< template x-if = "water()?.access_gate" >
< li > Gate< / li >
< / template >
< template x-if = "water()?.access_fence" >
< li > Fence< / li >
< / template >
< template x-if = "water()?.access_locked" >
< li > Locked< / li >
< / template >
< template x-if = "water()?.access_dog" >
< li > Dog< / li >
< / template >
< template x-if = "water()?.access_other" >
< li > Other access obstacle< / li >
< / template >
< / ul >
< / div >
< / div >
< / template >
< template x-if = "water()?.access_comments != ''" >
< div 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"
x-text="water()?.access_comments"
>< / div >
< / div >
< / template >
2026-03-06 22:07:48 +00:00
< 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"
2026-03-20 18:54:37 +00:00
:class="water()?.has_larvae ? 'badge-larvae' : 'bg-light text-muted'"
2026-03-06 22:07:48 +00:00
>
< i
class="bi"
2026-03-20 18:54:37 +00:00
:class="water()?.has_larvae ? 'bi-check-circle' : 'bi-circle'"
2026-03-06 22:07:48 +00:00
>< / i >
Larvae
< / span >
< span
class="badge me-2"
2026-03-20 18:54:37 +00:00
:class="water()?.has_pupae ? 'badge-pupae' : 'bg-light text-muted'"
2026-03-06 22:07:48 +00:00
>
< i
class="bi"
2026-03-20 18:54:37 +00:00
:class="water()?.has_pupae ? 'bi-check-circle' : 'bi-circle'"
2026-03-06 22:07:48 +00:00
>< / i >
Pupae
< / span >
< span
class="badge"
2026-03-20 18:54:37 +00:00
:class="water()?.has_adult ? 'badge-adult' : 'bg-light text-muted'"
2026-03-06 22:07:48 +00:00
>
< i
class="bi"
2026-03-20 18:54:37 +00:00
:class="water()?.has_adult ? 'bi-check-circle' : 'bi-circle'"
2026-03-06 22:07:48 +00:00
>< / i >
Adult Mosquitoes
< / span >
< / div >
2026-03-20 19:07:10 +00:00
< template x-if = "water()?.comments != ''" >
< div 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"
x-text="water()?.comments"
>< / div >
< / div >
< / template >
< 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"
x-text="water()?.owner.name || 'not given'"
>< / div >
< / div >
< div class = "col-md-6" >
< template x-if = "water()?.owner.has_email" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-envelope" > < / i >
< / label >
< / template >
< template x-if = "water()?.owner.has_phone" >
< label class = "form-label text-muted small mb-0" >
< i class = "bi bi-phone" > < / i >
< / label >
< / template >
< / div >
2026-03-06 22:07:48 +00:00
< / div >
< / div >
< / template >
2026-02-27 16:51:41 +00:00
2026-03-06 22:07:48 +00:00
<!-- 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"
2026-03-07 01:57:18 +00:00
x-text="selectedCommunication.public_report.images ? selectedCommunication.public_report.images.length : 0"
2026-03-06 22:07:48 +00:00
>< / span >
< / div >
< div class = "card-body" >
< template
2026-03-07 01:57:18 +00:00
x-if="selectedCommunication.public_report.images & & selectedCommunication.public_report.images.length > 0"
2026-03-06 22:07:48 +00:00
>
< div class = "d-flex flex-wrap gap-2" >
< template
2026-03-07 01:57:18 +00:00
x-for="(photo, index) in selectedCommunication.public_report.images"
2026-03-06 22:07:48 +00:00
:key="index"
>
< img
2026-03-09 19:29:48 +00:00
:src="photo.url_content"
2026-03-06 22:07:48 +00:00
class="photo-thumbnail"
@click="openPhotoViewer(index)"
:alt="'Photo ' + (index + 1)"
/>
< / template >
< / div >
< / template >
< template
2026-03-07 01:57:18 +00:00
x-if="!selectedCommunication.public_report.images || selectedCommunication.public_report.images.length === 0"
2026-03-06 22:07:48 +00:00
>
< div class = "text-muted text-center py-3" >
< i class = "bi bi-camera-slash fs-4" > < / i >
2026-03-07 01:57:18 +00:00
< p class = "mb-0 small" > No images attached< / p >
2026-03-06 22:07:48 +00:00
< / div >
< / template >
< / div >
< / div >
< / div >
2026-02-27 16:51:41 +00:00
< / div >
2026-03-06 22:07:48 +00:00
< / template >
< / div >
2026-02-27 16:51:41 +00:00
2026-03-06 22:07:48 +00:00
<!-- Right Column - Actions -->
< div class = "col-md-3 border-start p-0" >
2026-03-06 23:45:12 +00:00
< template x-if = "!selectedCommunication" >
2026-03-06 22:07:48 +00:00
< div
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 >
2026-02-27 16:51:41 +00:00
< / div >
2026-03-06 22:07:48 +00:00
< / template >
2026-02-27 16:51:41 +00:00
2026-03-06 23:45:12 +00:00
< template x-if = "selectedCommunication" >
2026-03-06 22:07:48 +00:00
< div 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" >
2026-03-19 17:41:38 +00:00
<!-- Create Signal -->
2026-03-06 22:07:48 +00:00
< div class = "d-grid mb-3" >
2026-03-19 17:41:38 +00:00
< button
class="btn btn-success btn-lg"
@click="createSignal()"
>
< i class = "bi bi-plus-circle me-2" > < / i > Mark Signal
2026-03-06 22:07:48 +00:00
< / button >
< small class = "text-muted mt-1" >
2026-03-19 17:41:38 +00:00
This report is useful signal
2026-03-06 22:07:48 +00:00
< / small >
< / div >
<!-- Mark Invalid -->
< div class = "d-grid mb-3" >
2026-03-14 01:14:30 +00:00
< button class = "btn btn-outline-danger" @ click = "markInvalid()" >
2026-03-06 22:07:48 +00:00
< i class = "bi bi-x-circle me-2" > < / i > Mark Invalid
< / button >
< small class = "text-muted mt-1" >
2026-03-19 17:41:38 +00:00
This report isn't useful
2026-03-06 22:07:48 +00:00
< / small >
< / div >
2026-02-27 16:51:41 +00:00
2026-03-06 22:07:48 +00:00
< hr / >
<!-- Message Reporter -->
2026-03-14 16:23:09 +00:00
< template
2026-03-14 20:04:10 +00:00
x-if="!(selectedCommunication?.public_report.reporter.has_email & & selectedCommunication?.public_report.reporter.has_phone)"
2026-03-14 16:23:09 +00:00
>
< div class = "mb-3" >
< h6 >
< i class = "bi bi-chat-dots" > < / i > No Reporter Communications
Available
< / h6 >
2026-03-06 22:07:48 +00:00
< / div >
2026-03-14 16:23:09 +00:00
< / template >
< template
2026-03-14 20:04:10 +00:00
x-if="selectedCommunication?.public_report.reporter.has_email || selectedCommunication?.public_report.reporter.has_phone"
2026-03-14 16:23:09 +00:00
>
< div 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< /label
>
< 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"
x-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 >
2026-03-06 22:07:48 +00:00
< / div >
2026-03-14 16:23:09 +00:00
< / template >
2026-03-06 22:07:48 +00:00
< hr / >
<!-- Report History -->
< div >
< h6 > < i class = "bi bi-clock-history" > < / i > Activity Log< / h6 >
< div class = "small" >
< template
2026-03-18 18:56:51 +00:00
x-for="entry in selectedCommunication.public_report.log || []"
:key="entry.created"
2026-03-06 22:07:48 +00:00
>
< div class = "border-start border-2 ps-2 mb-2" >
2026-03-18 18:56:51 +00:00
< template x-if = "entry.type == 'created'" >
< div >
< div class = "text-muted" > Initial Report< / div >
< small
class="text-muted"
x-text="formatDate(entry.created)"
>< / small >
< / div >
< / template >
< template x-if = "entry.type == 'message-text'" >
< div >
< div class = "text-muted" > Text Message< / div >
< div x-text = "entry.message" > < / div >
< small
class="text-muted"
x-text="formatDate(entry.created)"
>< / small >
< / div >
< / template >
< template
x-if="!(entry.type == 'created' || entry.type == 'message-text')"
>
< div x-text = "entry.type" > < / div >
< / template >
2026-03-06 22:07:48 +00:00
< / div >
< / template >
< template
2026-03-18 18:56:51 +00:00
x-if="!selectedCommunication.public_report.log || selectedCommunication.public_report.log.length === 0"
2026-03-06 22:07:48 +00:00
>
< div class = "text-muted" > No activity yet< / div >
< / template >
< / div >
< / div >
2026-02-27 16:51:41 +00:00
< / div >
< / div >
2026-03-06 22:07:48 +00:00
< / template >
< / div >
< / div >
< / div >
2026-02-27 16:51:41 +00:00
2026-03-06 22:07:48 +00:00
<!-- Photo Viewer Modal -->
< div
class="modal fade"
:class="{ 'show d-block': showPhotoModal }"
tabindex="-1"
x-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 < span x-text = "currentPhotoIndex + 1" > < / span > of
2026-03-07 01:57:18 +00:00
< span
x-text="selectedCommunication?.public_report.images.length || 0"
>< / span >
2026-03-06 22:07:48 +00:00
< / h5 >
< button
type="button"
class="btn-close"
@click="showPhotoModal = false"
>< / button >
< / div >
< div class = "modal-body text-center" >
2026-03-14 16:17:08 +00:00
< template x-if = "selectedCommunication && showPhotoModal" >
2026-03-09 19:32:19 +00:00
< div >
< 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
x-text="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
x-text="(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< /small
>
2026-03-10 15:50:39 +00:00
< template
2026-03-20 15:50:18 +00:00
x-if="selectedCommunication.public_report.images[currentPhotoIndex].location != null"
2026-03-10 15:50:39 +00:00
>
< span
x-text="formatDistance(selectedCommunication.public_report.images[currentPhotoIndex].distance_from_reporter_meters)"
>< / span >
< / template >
< template
2026-03-20 15:50:18 +00:00
x-if="selectedCommunication.public_report.images[currentPhotoIndex].location == null"
2026-03-10 15:50:39 +00:00
>
< span > No location data in image< / span >
< / template >
2026-03-09 19:32:19 +00:00
< / div >
< / div >
< / div >
< / div >
2026-03-06 22:07:48 +00:00
< / template >
< / 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"
2026-03-07 01:57:18 +00:00
@click="currentPhotoIndex = Math.min(selectedCommunication.public_report.images.length - 1, currentPhotoIndex + 1)"
:disabled="currentPhotoIndex >= (selectedCommunication?.public_report.images?.length || 1) - 1"
2026-03-06 22:07:48 +00:00
>
Next < i class = "bi bi-chevron-right" > < / i >
< / button >
2026-02-27 16:51:41 +00:00
< / div >
< / div >
< / div >
2026-03-06 22:07:48 +00:00
< / div >
< div
class="modal-backdrop fade show"
x-show="showPhotoModal"
@click="showPhotoModal = false"
>< / div >
2026-02-27 16:51:41 +00:00
2026-03-06 22:07:48 +00:00
<!-- 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" x-text = "toastTitle" > < / strong >
< button
type="button"
class="btn-close"
@click="showToast = false"
>< / button >
< / div >
< div class = "toast-body" x-text = "toastMessage" > < / div >
2026-02-27 16:51:41 +00:00
< / div >
2026-03-06 22:07:48 +00:00
< / div >
2026-02-27 16:51:41 +00:00
< / div >
2026-03-06 22:07:48 +00:00
< script src = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" > < / script >
2026-02-27 16:51:41 +00:00
{{ end }}