nidus-sync/ts/rmo/view/StatusByID.vue
Eli Ribble f88ca57d97
Migrate existing ts types from the API into the API module
This makes it possible to start hydrating the types into valid data
types like Dates which means I can get type safety guarantees when
displaying information.
2026-04-09 00:25:21 +00:00

201 lines
5.1 KiB
Vue

<style scoped>
.map-container {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
height: 500px;
margin-bottom: 20px;
margin-top: 20px;
align-items: center;
justify-content: center;
/* Prevent touch scrolling issues */
touch-action: pan-y pinch-zoom;
}
#map {
width: 100%;
height: 100%;
}
.status-badge {
font-size: 1rem;
}
.timeline {
border-left: 3px solid #dee2e6;
padding-left: 20px;
margin-left: 10px;
}
.timeline-item {
position: relative;
margin-bottom: 25px;
}
.timeline-item:before {
content: "";
position: absolute;
left: -29px;
top: 0;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #0d6efd;
}
.timeline-date {
font-size: 0.85rem;
color: #6c757d;
}
</style>
<template>
<HeaderDistrict :district="district" v-if="district" />
<Header v-else />
<div class="container my-4" v-if="report">
<!-- Report ID and Status Section -->
<div class="card mb-4">
<div
class="card-header bg-primary text-white d-flex justify-content-between align-items-center"
>
<h5 class="mb-0">Report {{ id }}</h5>
<span class="badge bg-warning text-dark status-badge">
{{ report.status }}
</span>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<strong><i class="bi bi-tag me-2"></i>Type:</strong>
<span>{{ report.type }}</span>
</div>
<div class="col-md-4 mb-3">
<strong><i class="bi bi-calendar me-2"></i>Created:</strong>
<span>{{ formatTimeRelative(report.created) }}</span>
</div>
<div class="col-md-4 mb-3" v-if="district">
<strong><i class="bi bi-crosshair me-2"></i>District:</strong>
<span>
{{ district.name }}
</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<strong><i class="bi bi-pin-map me-2"></i>Location:</strong>
<span>{{ report.address.raw }}</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<strong><i class="bi bi-images me-2"></i>Images:</strong>
<span>
{{
report.images.length > 0
? report.images.length
: "None provided"
}}
</span>
</div>
</div>
</div>
</div>
<!-- Map Section -->
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="bi bi-pin-map-fill me-2"></i>Location Map
</h5>
</div>
<div class="card-body p-0">
<div class="map-container">
<MapLocatorDisplay id="map" :markers="markers"></MapLocatorDisplay>
</div>
</div>
</div>
<!-- History Timeline -->
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="bi bi-clock-history me-2"></i>Request History
</h5>
</div>
<div class="card-body">
<div class="timeline">
<div
v-for="(item, index) in report.log"
:key="index"
class="timeline-item"
>
<div class="timeline-date">
{{ formatTimeRelative(item.created) }}
</div>
<h5 class="mb-1">{{ item.type }}</h5>
<p class="mb-0">{{ item.message }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="container my-4" v-else>
<p>loading...</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { computedAsync } from "@vueuse/core";
import Header from "@/rmo/components/Header.vue";
import HeaderDistrict from "@/components/HeaderDistrict.vue";
import MapLocatorDisplay from "@/components/MapLocatorDisplay.vue";
import { useStoreDistrict } from "@/rmo/store/district";
import { useStorePublicReport } from "@/store/publicreport";
import type { Marker } from "@/types";
import type { District, PublicReport } from "@/type/api";
import { formatTimeRelative } from "@/format";
// Props
interface Props {
id: string;
}
const props = defineProps<Props>();
const storeDistrict = useStoreDistrict();
const storePublicReport = useStorePublicReport();
// Computed
const report = computedAsync(async (): Promise<PublicReport | undefined> => {
return await storePublicReport.byID(props.id);
});
const district = computedAsync(async (): Promise<District | undefined> => {
if (!(report.value && report.value.district)) {
return undefined;
}
return await storeDistrict.byURI(report.value.district);
});
const markers = computed((): Marker[] => {
if (!(report.value && report.value.location)) {
return [];
}
return [
{
id: props.id,
location: report.value.location,
},
];
});
// Lifecycle
onMounted(() => {
// Load map scripts if needed
loadMapScripts();
});
const loadMapScripts = () => {
// Load MapLibre GL if not already loaded
if (!document.querySelector('script[src*="maplibre-gl"]')) {
const script1 = document.createElement("script");
script1.src = "//unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js";
document.head.appendChild(script1);
}
// Load Mapbox GL if not already loaded
if (!document.querySelector('script[src*="mapbox-gl"]')) {
const script2 = document.createElement("script");
script2.src =
"https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js";
document.head.appendChild(script2);
}
};
</script>