2026-03-22 03:55:42 +00:00
|
|
|
<style scoped>
|
|
|
|
|
.report-card {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.report-card:hover {
|
2026-03-22 04:08:16 +00:00
|
|
|
background-color: $secondary;
|
2026-03-22 03:55:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.report-card.active {
|
2026-03-22 04:08:16 +00:00
|
|
|
background-color: $primary;
|
2026-03-22 03:55:42 +00:00
|
|
|
color: white;
|
|
|
|
|
}
|
2026-03-22 06:40:31 +00:00
|
|
|
.reports-list {
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
max-height: 100vh;
|
|
|
|
|
}
|
2026-03-22 03:55:42 +00:00
|
|
|
</style>
|
|
|
|
|
|
2026-03-22 02:55:17 +00:00
|
|
|
<template>
|
2026-03-22 10:14:48 +00:00
|
|
|
<div class="card shadow-sm h-100 reports-list">
|
|
|
|
|
<div class="card-header bg-light pane-header">
|
2026-03-22 02:55:17 +00:00
|
|
|
<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..."
|
|
|
|
|
v-model="searchFilter"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-03-22 10:14:48 +00:00
|
|
|
</div>
|
|
|
|
|
<div class="card-body scroll-pane">
|
|
|
|
|
<div class="mb-3">
|
2026-03-22 02:55:17 +00:00
|
|
|
<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-22 10:14:48 +00:00
|
|
|
<i class="bi bi-mosquito"></i>Nuisance
|
2026-03-22 02:55:17 +00:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-sm"
|
|
|
|
|
:class="typeFilter === 'water' ? 'btn-info' : 'btn-outline-secondary'"
|
|
|
|
|
@click="typeFilter = 'water'"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi bi-droplet"></i> Water
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-22 10:14:48 +00:00
|
|
|
<div class="list-group list-group-flush">
|
2026-03-31 14:52:53 +00:00
|
|
|
<div v-if="loading || all == null" class="loading">Loading...</div>
|
2026-03-22 10:14:48 +00:00
|
|
|
<div
|
|
|
|
|
v-else-if="all.length > 0"
|
|
|
|
|
v-for="comm in filteredCommunications"
|
|
|
|
|
:key="comm.id"
|
|
|
|
|
class="border rounded list-group-item report-card p-3"
|
|
|
|
|
:class="{
|
|
|
|
|
active: selectedId && selectedId === comm.id,
|
|
|
|
|
}"
|
|
|
|
|
@click="handleClick(comm.id)"
|
|
|
|
|
>
|
|
|
|
|
<!-- First row: icon, type badge, and time -->
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
|
|
|
<div class="d-flex align-items-center">
|
|
|
|
|
<i
|
|
|
|
|
v-if="comm.type === 'publicreport.nuisance'"
|
|
|
|
|
class="bi bi-mosquito icon-nuisance fs-4 me-2"
|
|
|
|
|
>
|
|
|
|
|
</i>
|
|
|
|
|
<i
|
|
|
|
|
v-if="comm.type === 'publicreport.water'"
|
|
|
|
|
class="bi bi-droplet-fill icon-standing-water fs-4 me-2"
|
|
|
|
|
></i>
|
|
|
|
|
<span
|
|
|
|
|
class="badge"
|
|
|
|
|
:class="
|
|
|
|
|
comm.type === 'publicreport.nuisance'
|
|
|
|
|
? 'bg-danger'
|
|
|
|
|
: 'bg-info'
|
|
|
|
|
"
|
|
|
|
|
>
|
|
|
|
|
{{
|
|
|
|
|
comm.type === "publicreport.nuisance"
|
|
|
|
|
? "Nuisance"
|
|
|
|
|
: "Standing Water"
|
|
|
|
|
}}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<small>
|
|
|
|
|
<TimeRelative :time="comm.created" />
|
|
|
|
|
</small>
|
2026-03-22 02:55:17 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-22 10:14:48 +00:00
|
|
|
<!-- Details section: full width -->
|
2026-03-22 02:55:17 +00:00
|
|
|
<div>
|
2026-03-22 10:14:48 +00:00
|
|
|
<div>
|
|
|
|
|
<i class="bi bi-geo-alt text-muted"></i>
|
|
|
|
|
<span class="fw-medium">{{
|
2026-03-31 14:52:53 +00:00
|
|
|
comm.public_report?.address.postal_code
|
2026-03-22 10:14:48 +00:00
|
|
|
}}</span>
|
|
|
|
|
</div>
|
2026-03-31 14:52:53 +00:00
|
|
|
<small>{{ formatAddress(comm.public_report?.address) }}</small>
|
2026-03-22 10:14:48 +00:00
|
|
|
<div
|
|
|
|
|
v-if="
|
2026-03-31 14:52:53 +00:00
|
|
|
comm.public_report?.images &&
|
|
|
|
|
comm.public_report?.images.length > 0
|
2026-03-22 10:14:48 +00:00
|
|
|
"
|
|
|
|
|
class="mt-1"
|
|
|
|
|
>
|
|
|
|
|
<small class="text-muted">
|
|
|
|
|
<i class="bi bi-camera"></i>
|
|
|
|
|
{{ comm.public_report.images.length }} photo(s)
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
2026-03-22 02:55:17 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-if="filteredCommunications.length === 0"
|
|
|
|
|
class="text-center text-muted p-4"
|
|
|
|
|
>
|
|
|
|
|
<i class="bi bi-inbox fs-1"></i>
|
|
|
|
|
<p class="mt-2">No reports found</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-03-22 03:33:52 +00:00
|
|
|
import { computed, ref } from "vue";
|
2026-03-31 14:52:53 +00:00
|
|
|
import TimeRelative from "@/components/TimeRelative.vue";
|
|
|
|
|
import { formatAddress } from "@/format";
|
2026-04-09 00:25:21 +00:00
|
|
|
import { Communication, LogEntry, PublicReport } from "@/type/api";
|
2026-03-22 02:55:17 +00:00
|
|
|
|
2026-03-22 03:33:52 +00:00
|
|
|
interface Props {
|
2026-03-22 03:55:42 +00:00
|
|
|
all: Communication[] | null;
|
2026-03-22 03:33:52 +00:00
|
|
|
loading: boolean;
|
2026-03-22 03:55:42 +00:00
|
|
|
selectedId?: string | null;
|
2026-03-22 03:33:52 +00:00
|
|
|
}
|
2026-03-22 03:55:42 +00:00
|
|
|
interface Emits {
|
2026-03-22 18:25:02 +00:00
|
|
|
(e: "deselect", id: string): void;
|
2026-03-22 03:55:42 +00:00
|
|
|
(e: "select", id: string): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
2026-03-31 14:52:53 +00:00
|
|
|
selectedId: null,
|
2026-03-22 03:55:42 +00:00
|
|
|
});
|
2026-03-22 03:33:52 +00:00
|
|
|
|
2026-03-22 03:55:42 +00:00
|
|
|
const emit = defineEmits<Emits>();
|
|
|
|
|
const handleClick = (id: string) => {
|
2026-03-22 18:25:02 +00:00
|
|
|
if (props.selectedId == null) {
|
|
|
|
|
emit("select", id);
|
2026-03-27 06:25:43 -07:00
|
|
|
} else if (props.selectedId == id) {
|
2026-03-22 18:25:02 +00:00
|
|
|
emit("deselect", id);
|
2026-03-27 06:25:43 -07:00
|
|
|
} else {
|
|
|
|
|
emit("select", id);
|
2026-03-22 18:25:02 +00:00
|
|
|
}
|
2026-03-22 03:55:42 +00:00
|
|
|
};
|
2026-03-31 14:52:53 +00:00
|
|
|
const searchFilter = ref<string>("");
|
|
|
|
|
const typeFilter = ref<string>("all");
|
2026-03-22 02:55:17 +00:00
|
|
|
|
2026-03-22 03:55:42 +00:00
|
|
|
function selectCommunication(communication: Communication) {
|
|
|
|
|
// Emit both events - one for general use, one for v-model
|
|
|
|
|
console.log("selected", communication);
|
2026-03-31 14:52:53 +00:00
|
|
|
emit("select", communication.id);
|
|
|
|
|
//emit("update:selectedItem", communication);
|
2026-03-22 03:55:42 +00:00
|
|
|
//messageText.value = "";
|
|
|
|
|
//updateMap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 02:55:17 +00:00
|
|
|
// Computed properties
|
|
|
|
|
const filteredCommunications = computed(() => {
|
2026-03-22 03:33:52 +00:00
|
|
|
if (props.all == null) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return props.all.filter((c) => {
|
2026-03-22 02:55:17 +00:00
|
|
|
const matchesType =
|
|
|
|
|
typeFilter.value === "all" || c.type === typeFilter.value;
|
|
|
|
|
return matchesType && filterMatches(searchFilter.value, c);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-03-22 03:33:52 +00:00
|
|
|
// Methods
|
2026-03-31 14:52:53 +00:00
|
|
|
function filterMatches(filter: string, comm: Communication) {
|
|
|
|
|
const pr = comm.public_report;
|
|
|
|
|
// When we have non-public-report communications fix this.
|
|
|
|
|
if (pr == null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return filterMatchesPublicReport(filter, pr);
|
|
|
|
|
}
|
|
|
|
|
function filterMatchesLogEntry(filter: string, logs: LogEntry[]) {
|
|
|
|
|
for (const le of logs) {
|
|
|
|
|
if (le.message.includes(filter)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 03:33:52 +00:00
|
|
|
}
|
2026-03-31 14:52:53 +00:00
|
|
|
function filterMatchesPublicReport(filter: string, pr: PublicReport) {
|
|
|
|
|
if (
|
2026-04-09 00:25:21 +00:00
|
|
|
pr.address.raw.includes(filter) ||
|
2026-04-14 01:26:23 +00:00
|
|
|
pr.public_id.includes(filter) ||
|
2026-03-31 14:52:53 +00:00
|
|
|
filterMatchesLogEntry(filter, pr.log)
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
2026-03-22 03:33:52 +00:00
|
|
|
}
|
2026-03-31 14:52:53 +00:00
|
|
|
return false;
|
2026-03-22 03:33:52 +00:00
|
|
|
}
|
2026-03-22 02:55:17 +00:00
|
|
|
</script>
|