Add report rendering table to status page
This commit is contained in:
parent
5e638bdf1d
commit
c783ab7942
4 changed files with 268 additions and 10 deletions
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
197
ts/rmo/components/TableReport.vue
Normal file
197
ts/rmo/components/TableReport.vue
Normal 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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue