1053 lines
33 KiB
HTML
1053 lines
33 KiB
HTML
{{ 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>
|
|
<script
|
|
defer
|
|
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
|
|
></script>
|
|
<script src="/static/js/time-relative.js"></script>
|
|
<script src="/static/js/map-multipoint.js"></script>
|
|
<script>
|
|
function onLoad() {}
|
|
window.addEventListener("load", onLoad);
|
|
</script>
|
|
<script>
|
|
function filterMatches(filter, comm) {
|
|
return true;
|
|
}
|
|
function formatAddress(a) {
|
|
return a.number + " " + a.street + ", " + a.locality;
|
|
}
|
|
function communicationsApp() {
|
|
return {
|
|
apiBase: "/api",
|
|
// State
|
|
selectedCommunication: null,
|
|
searchFilter: "",
|
|
typeFilter: "all",
|
|
messageText: "",
|
|
showPhotoModal: false,
|
|
showInvalidModal: false,
|
|
currentPhotoIndex: 0,
|
|
invalidReason: "",
|
|
invalidNotes: "",
|
|
showToast: false,
|
|
toastTitle: "",
|
|
toastMessage: "",
|
|
|
|
// Sample data - replace with API call
|
|
communications: [
|
|
{
|
|
created: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
|
id: 1001,
|
|
public_report: {
|
|
address: {
|
|
postal_code: "32801",
|
|
},
|
|
location: {
|
|
latitude: 28.5383,
|
|
longitude: -81.3792,
|
|
},
|
|
reporter: {
|
|
name: "John Smith",
|
|
has_email: true,
|
|
},
|
|
tod_evening: true,
|
|
},
|
|
type: "nuisance",
|
|
|
|
timeOfDay: "Evening (6pm - 9pm)",
|
|
propertyAreas: ["Backyard", "Pool Area"],
|
|
notes:
|
|
"Mosquitoes have been extremely bad for the past week. Cannot enjoy outdoor activities anymore. They seem to be coming from the drainage ditch behind our property.",
|
|
photos: [
|
|
"https://via.placeholder.com/400x300/cccccc/666666?text=Photo+1",
|
|
"https://via.placeholder.com/400x300/cccccc/666666?text=Photo+2",
|
|
],
|
|
activityLog: [
|
|
{
|
|
action: "Report created",
|
|
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
|
|
init() {
|
|
this.loadFromAPI();
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
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",
|
|
});
|
|
});
|
|
});
|
|
},
|
|
// Computed property for filtered reports
|
|
get filteredCommunications() {
|
|
return this.communications.filter((report) => {
|
|
const matchesType =
|
|
this.typeFilter === "all" || report.type === this.typeFilter;
|
|
return matchesType && filterMatches(this.searchFilter, report);
|
|
});
|
|
},
|
|
|
|
async loadCommunications() {
|
|
try {
|
|
// Build query parameters from filters
|
|
const params = new URLSearchParams();
|
|
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();
|
|
this.communications = data.communications || data; // Handle different response formats
|
|
} catch (err) {
|
|
console.error("Error loading communications:", err);
|
|
throw err;
|
|
}
|
|
},
|
|
async loadFromAPI() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
try {
|
|
await Promise.all([this.loadCommunications()]);
|
|
} catch (err) {
|
|
this.error = err.message;
|
|
console.error("Error loading data:", err);
|
|
}
|
|
},
|
|
// Methods
|
|
selectCommunication(report) {
|
|
this.selectedCommunication = report;
|
|
this.messageText = "";
|
|
this.updateMap();
|
|
},
|
|
|
|
formatDate(date) {
|
|
return new Date(date).toLocaleString();
|
|
},
|
|
|
|
openPhotoViewer(index) {
|
|
this.currentPhotoIndex = index;
|
|
this.showPhotoModal = true;
|
|
},
|
|
|
|
applyMessageTemplate(template) {
|
|
const templates = {
|
|
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`,
|
|
};
|
|
|
|
if (templates[template]) {
|
|
this.messageText = templates[template];
|
|
}
|
|
},
|
|
|
|
createLead() {
|
|
// TODO: Implement API call to create lead
|
|
console.log(
|
|
"Creating lead for report:",
|
|
this.selectedCommunication.id,
|
|
);
|
|
|
|
// Add to activity log
|
|
if (!this.selectedCommunication.activityLog) {
|
|
this.selectedCommunication.activityLog = [];
|
|
}
|
|
this.selectedCommunication.activityLog.push({
|
|
action: "Lead created",
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
this.showNotification(
|
|
"Lead Created",
|
|
`Lead successfully created for report #${this.selectedCommunication.id}`,
|
|
);
|
|
|
|
// Remove from list after creating lead
|
|
// this.communications = this.communications.filter(r => r.id !== this.selectedCommunication.id);
|
|
// this.selectedCommunication = null;
|
|
},
|
|
|
|
markInvalid() {
|
|
// TODO: Implement API call to mark as invalid
|
|
console.log(
|
|
"Marking report as invalid:",
|
|
this.selectedCommunication.id,
|
|
this.invalidReason,
|
|
this.invalidNotes,
|
|
);
|
|
|
|
this.showNotification(
|
|
"Report Marked Invalid",
|
|
`Report #${this.selectedCommunication.id} has been marked as ${this.invalidReason}`,
|
|
);
|
|
|
|
// Remove from list
|
|
this.communications = this.communications.filter(
|
|
(r) => r.id !== this.selectedCommunication.id,
|
|
);
|
|
this.selectedCommunication = null;
|
|
this.showInvalidModal = false;
|
|
this.invalidReason = "";
|
|
this.invalidNotes = "";
|
|
},
|
|
|
|
sendMessage() {
|
|
if (!this.messageText.trim()) return;
|
|
|
|
// TODO: Implement API call to send message
|
|
console.log(
|
|
"Sending message to:",
|
|
this.selectedCommunication.public_report.reporter.has_email,
|
|
);
|
|
console.log("Message:", this.messageText);
|
|
|
|
// Add to activity log
|
|
if (!this.selectedCommunication.activityLog) {
|
|
this.selectedCommunication.activityLog = [];
|
|
}
|
|
this.selectedCommunication.activityLog.push({
|
|
action: "Message sent to reporter",
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
this.showNotification(
|
|
"Message Sent",
|
|
`Message successfully sent to ${this.selectedCommunication.public_report.reporter.name}`,
|
|
);
|
|
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",
|
|
);
|
|
},
|
|
updateMap() {
|
|
const map = document.querySelector("map-multipoint");
|
|
const p = {
|
|
latitude:
|
|
this.selectedCommunication.public_report.location.latitude,
|
|
longitude:
|
|
this.selectedCommunication.public_report.location.longitude,
|
|
};
|
|
map.SetMarkers([p]);
|
|
const bounds = new maplibregl.LngLatBounds(
|
|
new maplibregl.LngLat(p.longitude - 0.01, p.latitude - 0.01),
|
|
new maplibregl.LngLat(p.longitude + 0.01, p.latitude + 0.01),
|
|
);
|
|
|
|
map.FitBounds(bounds, {
|
|
padding: 50,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
{{ end }}
|
|
{{ define "content" }}
|
|
<div x-data="communicationsApp()" class="h-100">
|
|
<div class="container-fluid h-100">
|
|
<div class="row h-100">
|
|
<!-- Left Column - Communications List -->
|
|
<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'"
|
|
>
|
|
<i class="bi">{{ template "mosquito.svg" }}</i> Nuisance
|
|
</button>
|
|
<button
|
|
class="btn btn-sm"
|
|
:class="typeFilter === 'standing_water' ? 'btn-info' : 'btn-outline-secondary'"
|
|
@click="typeFilter = 'standing_water'"
|
|
>
|
|
<i class="bi bi-droplet"></i> Water
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="list-group list-group-flush">
|
|
<template x-for="comm in filteredCommunications" :key="comm.id">
|
|
<div
|
|
class="list-group-item report-card p-3"
|
|
:class="{ 'active': selectedCommunication && selectedCommunication.id === comm.id }"
|
|
@click="selectCommunication(comm)"
|
|
>
|
|
<!-- 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">
|
|
<template x-if="comm.type === 'nuisance'">
|
|
<i class="bi fs-4 me-2">{{ template "mosquito.svg" }}</i>
|
|
</template>
|
|
<template x-if="comm.type === 'standing_water'">
|
|
<i
|
|
class="bi bi-droplet-fill icon-standing-water fs-4 me-2"
|
|
></i>
|
|
</template>
|
|
<span
|
|
class="badge"
|
|
:class="comm.type === 'nuisance' ? 'bg-danger' : 'bg-info'"
|
|
x-text="comm.type === 'nuisance' ? 'Nuisance' : 'Standing Water'"
|
|
></span>
|
|
</div>
|
|
<small
|
|
><time-relative :time="comm.created"></time-relative>
|
|
</small>
|
|
</div>
|
|
|
|
<!-- Details section: full width -->
|
|
<div>
|
|
<div>
|
|
<i class="bi bi-geo-alt text-muted"></i>
|
|
<span
|
|
x-text="comm.public_report.address.postal_code"
|
|
class="fw-medium"
|
|
></span>
|
|
</div>
|
|
<small
|
|
x-text="formatAddress(comm.public_report.address).substring(0, 30) + '...'"
|
|
></small>
|
|
<template
|
|
x-if="comm.public_report.images && comm.public_report.images.length > 0"
|
|
>
|
|
<div class="mt-1">
|
|
<small class="text-muted">
|
|
<i class="bi bi-camera"></i>
|
|
<span x-text="comm.public_report.images.length"></span>
|
|
photo(s)
|
|
</small>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<template x-if="filteredCommunications.length === 0">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Middle Column - Report Details -->
|
|
<div class="col-md-6 p-0">
|
|
<div class="p-3">
|
|
<div class="map-container">
|
|
<map-multipoint
|
|
id="map"
|
|
organization-id="{{ .Organization.ID }}"
|
|
tegola="{{ .URL.Tegola }}"
|
|
xmin="{{ .Organization.ServiceAreaXmin.GetOr 0 }}"
|
|
ymin="{{ .Organization.ServiceAreaYmin.GetOr 0 }}"
|
|
xmax="{{ .Organization.ServiceAreaXmax.GetOr 0 }}"
|
|
ymax="{{ .Organization.ServiceAreaYmax.GetOr 0 }}"
|
|
></map-multipoint>
|
|
</div>
|
|
</div>
|
|
<template x-if="!selectedCommunication">
|
|
<div
|
|
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>
|
|
</template>
|
|
|
|
<template x-if="selectedCommunication">
|
|
<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">
|
|
<template
|
|
x-if="selectedCommunication.type === 'nuisance'"
|
|
>
|
|
<span
|
|
><i class="bi icon-nuisance"
|
|
>{{ template "mosquito.svg" }}</i
|
|
>
|
|
Nuisance Report</span
|
|
>
|
|
</template>
|
|
<template
|
|
x-if="selectedCommunication.type === 'standing_water'"
|
|
>
|
|
<span
|
|
><i
|
|
class="bi bi-droplet-fill icon-standing-water"
|
|
></i>
|
|
Standing Water Report</span
|
|
>
|
|
</template>
|
|
</h5>
|
|
<small class="text-muted"
|
|
>Report ID: #<span
|
|
x-text="selectedCommunication.id"
|
|
></span
|
|
></small>
|
|
</div>
|
|
<span class="badge bg-secondary"
|
|
><time-relative
|
|
:time="selectedCommunication.created"
|
|
></time-relative
|
|
></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"
|
|
x-text="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"
|
|
x-text="selectedCommunication.public_report.reporter.name"
|
|
></div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nuisance-specific Fields -->
|
|
<template x-if="selectedCommunication.type === 'nuisance'">
|
|
<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>
|
|
<ul>
|
|
<template
|
|
x-if="selectedCommunication.public_report.time_of_day_early"
|
|
>
|
|
<li>Early</li>
|
|
</template>
|
|
<template
|
|
x-if="selectedCommunication.public_report.time_of_day_day"
|
|
>
|
|
<li>Daytime</li>
|
|
</template>
|
|
<template
|
|
x-if="selectedCommunication.public_report.time_of_day_evening"
|
|
>
|
|
<li>Evening</li>
|
|
</template>
|
|
<template
|
|
x-if="selectedCommunication.public_report.time_of_day_night"
|
|
>
|
|
<li>Night</li>
|
|
</template>
|
|
</ul>
|
|
<div
|
|
class="fw-medium"
|
|
x-text="selectedCommunication.timeOfDay"
|
|
></div>
|
|
</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>
|
|
<template
|
|
x-if="selectedCommunication.public_report.is_location_backyard"
|
|
>
|
|
<li>Backyard</li>
|
|
</template>
|
|
<template
|
|
x-if="selectedCommunication.public_report.is_location_frontyard"
|
|
>
|
|
<li>Frontyard</li>
|
|
</template>
|
|
<template
|
|
x-if="selectedCommunication.public_report.is_location_garden"
|
|
>
|
|
<li>Garden</li>
|
|
</template>
|
|
<template
|
|
x-if="selectedCommunication.public_report.is_location_other"
|
|
>
|
|
<li>Other</li>
|
|
</template>
|
|
<template
|
|
x-if="selectedCommunication.public_report.is_location_pool"
|
|
>
|
|
<li>Pool</li>
|
|
</template>
|
|
</ul>
|
|
<template
|
|
x-for="area in selectedCommunication.propertyAreas"
|
|
:key="area"
|
|
>
|
|
<span
|
|
class="badge bg-secondary me-1"
|
|
x-text="area"
|
|
></span>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<template
|
|
x-if="selectedCommunication.public_report.source_description != ''"
|
|
>
|
|
<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"
|
|
x-text="selectedCommunication.public_report.source_description || 'none'"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
<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"
|
|
x-text="selectedCommunication.public_report.additional_info || 'No additional notes'"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Standing Water-specific Fields -->
|
|
<template
|
|
x-if="selectedCommunication.type === 'standing_water'"
|
|
>
|
|
<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">
|
|
<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="selectedCommunication.observedLarvae ? 'badge-larvae' : 'bg-light text-muted'"
|
|
>
|
|
<i
|
|
class="bi"
|
|
:class="selectedCommunication.observedLarvae ? 'bi-check-circle' : 'bi-circle'"
|
|
></i>
|
|
Larvae
|
|
</span>
|
|
<span
|
|
class="badge me-2"
|
|
:class="selectedCommunication.observedPupae ? 'badge-pupae' : 'bg-light text-muted'"
|
|
>
|
|
<i
|
|
class="bi"
|
|
:class="selectedCommunication.observedPupae ? 'bi-check-circle' : 'bi-circle'"
|
|
></i>
|
|
Pupae
|
|
</span>
|
|
<span
|
|
class="badge"
|
|
:class="selectedCommunication.observedAdult ? 'badge-adult' : 'bg-light text-muted'"
|
|
>
|
|
<i
|
|
class="bi"
|
|
:class="selectedCommunication.observedAdult ? 'bi-check-circle' : 'bi-circle'"
|
|
></i>
|
|
Adult Mosquitoes
|
|
</span>
|
|
</div>
|
|
<template x-if="selectedCommunication.waterSourceType">
|
|
<div class="mt-3">
|
|
<label class="form-label text-muted small mb-0">
|
|
<i class="bi bi-water"></i> Water Source Type
|
|
</label>
|
|
<div
|
|
class="fw-medium"
|
|
x-text="selectedCommunication.waterSourceType"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 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"
|
|
x-text="selectedCommunication.public_report.images ? selectedCommunication.public_report.images.length : 0"
|
|
></span>
|
|
</div>
|
|
<div class="card-body">
|
|
<template
|
|
x-if="selectedCommunication.public_report.images && selectedCommunication.public_report.images.length > 0"
|
|
>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<template
|
|
x-for="(photo, index) in selectedCommunication.public_report.images"
|
|
:key="index"
|
|
>
|
|
<img
|
|
:src="photo.url_content"
|
|
class="photo-thumbnail"
|
|
@click="openPhotoViewer(index)"
|
|
:alt="'Photo ' + (index + 1)"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<template
|
|
x-if="!selectedCommunication.public_report.images || selectedCommunication.public_report.images.length === 0"
|
|
>
|
|
<div 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>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Right Column - Actions -->
|
|
<div class="col-md-3 border-start p-0">
|
|
<template x-if="!selectedCommunication">
|
|
<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>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="selectedCommunication">
|
|
<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">
|
|
<!-- Create Lead -->
|
|
<div class="d-grid mb-3">
|
|
<button class="btn btn-success btn-lg" @click="createLead()">
|
|
<i class="bi bi-plus-circle me-2"></i>Create Lead
|
|
</button>
|
|
<small class="text-muted mt-1">
|
|
Creates a new service lead from this report
|
|
</small>
|
|
</div>
|
|
|
|
<!-- Mark Invalid -->
|
|
<div class="d-grid mb-3">
|
|
<button
|
|
class="btn btn-outline-danger"
|
|
@click="showInvalidModal = true"
|
|
>
|
|
<i class="bi bi-x-circle me-2"></i>Mark Invalid
|
|
</button>
|
|
<small class="text-muted mt-1">
|
|
Mark as spam, duplicate, or out of district
|
|
</small>
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<!-- Message Reporter -->
|
|
<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>
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<!-- Report History -->
|
|
<div>
|
|
<h6><i class="bi bi-clock-history"></i> Activity Log</h6>
|
|
<div class="small">
|
|
<template
|
|
x-for="activity in selectedCommunication.activityLog || []"
|
|
:key="activity.timestamp"
|
|
>
|
|
<div class="border-start border-2 ps-2 mb-2">
|
|
<div class="text-muted" x-text="activity.action"></div>
|
|
<small
|
|
class="text-muted"
|
|
x-text="formatDate(activity.timestamp)"
|
|
></small>
|
|
</div>
|
|
</template>
|
|
<template
|
|
x-if="!selectedCommunication.activityLog || selectedCommunication.activityLog.length === 0"
|
|
>
|
|
<div class="text-muted">No activity yet</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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
|
|
<span
|
|
x-text="selectedCommunication?.public_report.images.length || 0"
|
|
></span>
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showPhotoModal = false"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<template
|
|
x-if="selectedCommunication && selectedCommunication.public_report.images"
|
|
>
|
|
<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
|
|
>
|
|
<span
|
|
x-text="selectedCommunication.public_report.images[currentPhotoIndex].exif?.distance_from_reporter || 'N/A'"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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"
|
|
@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"
|
|
x-show="showPhotoModal"
|
|
@click="showPhotoModal = false"
|
|
></div>
|
|
|
|
<!-- Invalid Report Modal -->
|
|
<div
|
|
class="modal fade"
|
|
:class="{ 'show d-block': showInvalidModal }"
|
|
tabindex="-1"
|
|
x-show="showInvalidModal"
|
|
@click.self="showInvalidModal = false"
|
|
>
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-x-circle text-danger"></i> Mark as Invalid
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showInvalidModal = false"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Please select a reason for marking this report as invalid:</p>
|
|
<div class="form-check mb-2">
|
|
<input
|
|
class="form-check-input"
|
|
type="radio"
|
|
name="invalidReason"
|
|
id="reason1"
|
|
value="spam"
|
|
x-model="invalidReason"
|
|
/>
|
|
<label class="form-check-label" for="reason1">Spam or junk</label>
|
|
</div>
|
|
<div class="form-check mb-2">
|
|
<input
|
|
class="form-check-input"
|
|
type="radio"
|
|
name="invalidReason"
|
|
id="reason2"
|
|
value="duplicate"
|
|
x-model="invalidReason"
|
|
/>
|
|
<label class="form-check-label" for="reason2"
|
|
>Duplicate report</label
|
|
>
|
|
</div>
|
|
<div class="form-check mb-2">
|
|
<input
|
|
class="form-check-input"
|
|
type="radio"
|
|
name="invalidReason"
|
|
id="reason3"
|
|
value="out_of_district"
|
|
x-model="invalidReason"
|
|
/>
|
|
<label class="form-check-label" for="reason3"
|
|
>Outside service district</label
|
|
>
|
|
</div>
|
|
<div class="form-check mb-2">
|
|
<input
|
|
class="form-check-input"
|
|
type="radio"
|
|
name="invalidReason"
|
|
id="reason4"
|
|
value="insufficient"
|
|
x-model="invalidReason"
|
|
/>
|
|
<label class="form-check-label" for="reason4"
|
|
>Insufficient information</label
|
|
>
|
|
</div>
|
|
<div class="form-check mb-3">
|
|
<input
|
|
class="form-check-input"
|
|
type="radio"
|
|
name="invalidReason"
|
|
id="reason5"
|
|
value="other"
|
|
x-model="invalidReason"
|
|
/>
|
|
<label class="form-check-label" for="reason5">Other</label>
|
|
</div>
|
|
<textarea
|
|
class="form-control"
|
|
rows="2"
|
|
placeholder="Additional notes (optional)"
|
|
x-model="invalidNotes"
|
|
></textarea>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
@click="showInvalidModal = false"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger"
|
|
@click="markInvalid()"
|
|
:disabled="!invalidReason"
|
|
>
|
|
Confirm Invalid
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="modal-backdrop fade show"
|
|
x-show="showInvalidModal"
|
|
@click="showInvalidModal = 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" x-text="toastTitle"></strong>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showToast = false"
|
|
></button>
|
|
</div>
|
|
<div class="toast-body" x-text="toastMessage"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
{{ end }}
|