Add report rendering table to status page

This commit is contained in:
Eli Ribble 2026-04-24 23:06:07 +00:00
parent 5e638bdf1d
commit c783ab7942
No known key found for this signature in database
4 changed files with 268 additions and 10 deletions

View file

@ -6,12 +6,15 @@
import maplibregl from "maplibre-gl";
import { inject, onMounted, onBeforeUnmount, Ref, useAttrs, watch } from "vue";
export type MapEventType = maplibregl.MapEventType;
export type MouseEvent = maplibregl.MapLayerMouseEvent;
export type Feature = maplibregl.MapGeoJSONFeature;
type LayerType = maplibregl.LayerSpecification["type"];
interface Emits {
(e: "click", evt: MouseEvent): void;
(e: "mouseenter"): void;
(e: "mouseleave"): void;
(e: "update:modelValue", features: Feature[]): void;
}
export interface Props {
filter?: maplibregl.FilterSpecification;
@ -26,7 +29,7 @@ const attrs = useAttrs();
const emit = defineEmits<Emits>();
const props = withDefaults(defineProps<Props>(), {});
type OnCallbackFunc = (e?: MouseEvent) => void;
type OnCallbackFunc = (e?: any) => void;
type RegisterOnFunc = (
eventname: string,
layerid: string,
@ -37,6 +40,7 @@ type UnregisterLayerFunc = (id: string) => void;
const map: Ref<maplibregl.Map | null> | undefined = inject("map");
const registerOn: RegisterOnFunc | undefined = inject("registerOn");
const registerOnce: RegisterOnFunc | undefined = inject("registerOnce");
const registerLayer: RegisterLayerFunc | undefined = inject("registerLayer");
const unregisterLayer: UnregisterLayerFunc | undefined =
inject("unregisterLayer");
@ -54,6 +58,18 @@ const getLayerConfig = (): maplibregl.LayerSpecification => {
return result;
};
function updateModel() {
if (!(map && map.value)) return;
const query: maplibregl.QueryRenderedFeaturesOptions = {
layers: [props.id],
};
const features = map.value.queryRenderedFeatures(query);
const features_from_source = features.filter(
(feature: any) => feature.source == props.source,
);
emit("update:modelValue", features_from_source);
//emit("mouseleave");
}
onMounted(() => {
if (registerLayer) {
registerLayer(props.id, getLayerConfig());
@ -75,6 +91,12 @@ onMounted(() => {
emit("mouseleave");
});
}
if (registerOn) {
registerOn("moveend", props.id, updateModel);
}
if (registerOnce) {
registerOnce("idle", props.id, updateModel);
}
});
onBeforeUnmount(() => {

View file

@ -47,6 +47,7 @@ provide("map", map);
// Registry for tracking child components
const ons = new Map();
const onces = new Map();
const sources = new Map();
const layers = new Map();
@ -69,6 +70,24 @@ provide(
}
},
);
provide(
"registerOnce",
(
eventname: keyof maplibregl.MapLayerEventType,
layerid: string,
callback: OnCallbackFunc,
) => {
console.log("register map.once", eventname, layerid);
onces.set(`${eventname}.${layerid}`, {
callback: callback,
eventname: eventname,
layerid: layerid,
});
if (map.value && map.value.loaded()) {
map.value.once(eventname, layerid, callback);
}
},
);
provide("registerSource", (id: string, config: any) => {
console.log("register source", id, config);
sources.set(id, config);
@ -143,6 +162,10 @@ function initializeMap() {
_map.on(config.eventname, config.layerid, config.callback);
});
});
onces.forEach((config, id) => {
console.log("adding map.on", config.eventname, config.layerid);
_map.once(config.eventname, config.layerid, config.callback);
});
map.value = _map;
}
onMounted(() => {

View file

@ -0,0 +1,197 @@
<style scoped>
.table {
width: 100%;
margin-bottom: 0;
border-collapse: collapse;
}
.table-light {
background-color: #f8f9fa;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.075);
}
th,
td {
padding: 0.75rem;
border-bottom: 1px solid #dee2e6;
text-align: left;
}
.clickable-row {
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
.clickable-row:hover {
background-color: rgba(13, 110, 253, 0.1);
}
.badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: 0.75em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}
.bg-danger {
background-color: #dc3545;
}
.bg-primary {
background-color: #0d6efd;
}
.bg-success {
background-color: #198754;
}
.bg-warning {
background-color: #ffc107;
}
.bg-info {
background-color: #0dcaf0;
}
.bg-secondary {
background-color: #6c757d;
}
.report-type-badge {
font-size: 0.85rem;
}
.text-dark {
color: #212529 !important;
}
</style>
<template>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th scope="col">Report ID</th>
<th scope="col">Reported</th>
<th scope="col">Type</th>
<th scope="col">Address</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="report-table-body">
<tr
v-if="reports.length > 0"
v-for="report in reports"
:key="report.id"
class="clickable-row"
:data-report-id="report.id"
@click="handleRowClick(report.id)"
>
<td>
<strong>{{ formatId(report.id) }}</strong>
</td>
<td><TimeRelative :time="report.created" /></td>
<td>
<span
class="badge report-type-badge"
:class="getTypeClass(report.type)"
>
{{ report.type }}
</span>
</td>
<td>{{ report.address || "N/A" }}</td>
<td>
<span class="badge" :class="getStatusClass(report.status)">
{{ report.status }}
</span>
</td>
</tr>
<tr v-else>
<td colspan="5">No reports</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import TimeRelative from "@/components/TimeRelative.vue";
export interface Report {
id: string;
created: Date;
type: string;
address?: string;
status: string;
}
interface Props {
reports?: Report[];
}
// Define props with defaults
const props = withDefaults(defineProps<Props>(), {
reports: () => [],
});
// Define emits
const emit = defineEmits<{
(e: "row-clicked", reportId: string): void;
}>();
/**
* Get badge color class based on report type
*/
const getTypeClass = (type: string): string => {
switch (type) {
case "nuisance":
return "bg-danger";
case "quick":
return "bg-primary";
case "water":
return "bg-success";
default:
return "bg-secondary";
}
};
/**
* Get badge color class based on report status
*/
const getStatusClass = (status: string): string => {
switch (status) {
case "Reported":
return "bg-warning text-dark";
case "Assigned":
return "bg-info text-dark";
case "On-Hold":
return "bg-secondary";
case "Complete":
return "bg-success";
default:
return "bg-secondary";
}
};
/**
* Format the report ID with hyphens
*/
const formatId = (id: string): string => {
if (id.length === 12) {
return `${id.substring(0, 4)}-${id.substring(4, 8)}-${id.substring(8)}`;
}
return id;
};
/**
* Handle row click event
*/
const handleRowClick = (reportId: string): void => {
emit("row-clicked", reportId);
};
</script>

View file

@ -141,6 +141,7 @@
source="tegola"
sourceLayer="nuisance_location"
type="circle"
v-model="renderedReportsNuisance"
/>
<Source id="tegola" type="vector" :tiles="[tegola]" />
</Map>
@ -162,9 +163,7 @@
</div>
<div class="card-body p-0">
<div class="table-responsive">
<!--
<table-report />
-->
<TableReport :reports="renderedReports" />
</div>
</div>
<!--
@ -192,16 +191,15 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import Map from "@/map/Map.vue";
import Layer, { MouseEvent } from "@/map/Layer.vue";
import Source from "@/map/Source.vue";
import TableReport, { Report } from "@/rmo/components/TableReport.vue";
import { apiClient } from "@/client";
import Map from "@/map/Map.vue";
import Layer, { Feature, MouseEvent } from "@/map/Layer.vue";
import Source from "@/map/Source.vue";
import { useStoreAPI } from "@/store/api";
const storeAPI = useStoreAPI();
const tegola = ref<string | null>(null);
const paintConfigNuisance = {
"circle-color": "#DC4535",
"circle-radius": 7,
@ -214,6 +212,24 @@ const paintConfigWater = {
"circle-stroke-color": "#024AB6",
"circle-stroke-width": 2,
};
const renderedReportsNuisance = ref<Feature[]>([]);
const storeAPI = useStoreAPI();
const tegola = ref<string | null>(null);
const renderedReports = computed((): Report[] => {
let reports: Report[] = [];
renderedReportsNuisance.value.forEach((f) => {
const p = f.properties;
reports.push({
id: p.public_id,
created: new Date(p.created),
type: "nuisance",
address: p.address_raw,
status: p.status,
});
});
return reports;
});
onMounted(() => {
const a = storeAPI.get().then((a) => {
tegola.value = a.tegola.rmo;