2026-04-23 23:02:53 +00:00
|
|
|
<style scoped>
|
|
|
|
|
.map-container {
|
|
|
|
|
background-color: #e9ecef;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|
|
|
|
height: 500px;
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
2026-04-16 17:14:57 +00:00
|
|
|
<template>
|
|
|
|
|
<!-- Dashboard Header -->
|
|
|
|
|
<div class="row mb-4">
|
|
|
|
|
<div class="col-md-6">
|
2026-04-17 17:51:02 +00:00
|
|
|
<h1>{{ session?.organization?.name }} Dashboard</h1>
|
2026-04-16 17:14:57 +00:00
|
|
|
<p class="text-muted">
|
2026-04-17 17:51:02 +00:00
|
|
|
Hey {{ session?.self?.display_name }}, here's an overview of mosquito
|
|
|
|
|
control activities in your district
|
2026-04-16 17:14:57 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="col-md-6 text-md-end d-flex align-items-center justify-content-md-end"
|
|
|
|
|
>
|
|
|
|
|
<p v-if="dashboard.isSyncOngoing" class="last-refreshed mb-0">
|
|
|
|
|
<i class="fas fa-sync-alt me-2 syncing"></i>Syncing now...
|
|
|
|
|
</p>
|
|
|
|
|
<p v-else class="last-refreshed mb-0">
|
|
|
|
|
<i class="fas fa-sync-alt me-2"></i>Last updated:
|
|
|
|
|
<span id="last-refreshed-time">{{
|
|
|
|
|
formatTimeRelative(dashboard.lastSync)
|
|
|
|
|
}}</span>
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-sm btn-outline-primary ms-3"
|
|
|
|
|
@click="refreshData"
|
|
|
|
|
>
|
|
|
|
|
Refresh Data
|
|
|
|
|
</button>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Key Metrics -->
|
|
|
|
|
<div class="row g-4">
|
|
|
|
|
<!-- Last Refreshed -->
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<div class="card stats-card h-100">
|
|
|
|
|
<div class="card-body text-center">
|
|
|
|
|
<div class="metric-icon bg-success text-white">
|
|
|
|
|
<i class="fas fa-clock"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h5 class="card-title">Last Data Refresh</h5>
|
|
|
|
|
<p class="metric-value">
|
|
|
|
|
{{ formatTimeRelative(dashboard.lastSync) }}
|
|
|
|
|
</p>
|
|
|
|
|
<!-- <p class="card-text text-muted">Last sync: 12:45 PM</p> -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Service Requests -->
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<div class="card stats-card h-100">
|
|
|
|
|
<div class="card-body text-center">
|
|
|
|
|
<div class="metric-icon bg-warning text-white">
|
|
|
|
|
<i class="fas fa-ticket-alt"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h5 class="card-title">Service Requests</h5>
|
|
|
|
|
<p v-if="dashboard.isSyncOngoing" class="metric-value">
|
|
|
|
|
{{ formatBigNumber(dashboard.counts.service_requests) }}...?
|
|
|
|
|
</p>
|
|
|
|
|
<p v-else class="metric-value">
|
|
|
|
|
{{ formatBigNumber(dashboard.counts.service_requests) }}
|
|
|
|
|
</p>
|
|
|
|
|
<!--<p class="card-text text-muted">
|
|
|
|
|
<span class="text-success">
|
|
|
|
|
<i class="fas fa-arrow-up"></i> 12%
|
|
|
|
|
</span> since last week
|
|
|
|
|
</p>-->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Mosquito Sources -->
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<div class="card stats-card h-100">
|
|
|
|
|
<div class="card-body text-center">
|
|
|
|
|
<div class="metric-icon bg-danger text-white">
|
|
|
|
|
<i class="fas fa-bug"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h5 class="card-title">Mosquito Sources</h5>
|
|
|
|
|
<p v-if="dashboard.isSyncOngoing" class="metric-value">
|
|
|
|
|
{{ formatBigNumber(dashboard.counts.mosquito_sources) }}..?
|
|
|
|
|
</p>
|
|
|
|
|
<p v-else class="metric-value">
|
|
|
|
|
{{ formatBigNumber(dashboard.counts.mosquito_sources) }}
|
|
|
|
|
</p>
|
|
|
|
|
<!-- <p class="card-text text-muted">
|
|
|
|
|
<span class="text-danger">
|
|
|
|
|
<i class="fas fa-arrow-up"></i> 8%
|
|
|
|
|
</span> since last month
|
|
|
|
|
</p> -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Inspections -->
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<div class="card stats-card h-100">
|
|
|
|
|
<div class="card-body text-center">
|
|
|
|
|
<div class="metric-icon bg-info text-white">
|
|
|
|
|
<i class="fas fa-clipboard-check"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h5 class="card-title">Traps</h5>
|
|
|
|
|
<p v-if="dashboard.isSyncOngoing" class="metric-value">
|
|
|
|
|
{{ formatBigNumber(dashboard.counts.traps) }}...?
|
|
|
|
|
</p>
|
|
|
|
|
<p v-else class="metric-value">
|
|
|
|
|
{{ formatBigNumber(dashboard.counts.traps) }}
|
|
|
|
|
</p>
|
|
|
|
|
<!-- <p class="card-text text-muted">
|
|
|
|
|
<span class="text-success">
|
|
|
|
|
<i class="fas fa-arrow-up"></i> 15%
|
|
|
|
|
</span> since last week
|
|
|
|
|
</p> -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Map Section -->
|
|
|
|
|
<h3 class="section-title">Mosquito Activity Heatmap</h3>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-12">
|
2026-04-23 23:02:53 +00:00
|
|
|
<div class="map-container">
|
|
|
|
|
<Map
|
|
|
|
|
:bounds="mapBounds()"
|
2026-04-24 00:36:18 +00:00
|
|
|
:cursor="mapCursor"
|
2026-04-23 23:02:53 +00:00
|
|
|
class="map"
|
|
|
|
|
:markers="[]"
|
|
|
|
|
>
|
|
|
|
|
<Layer
|
2026-04-24 13:23:03 +00:00
|
|
|
@click="doClickMap"
|
2026-04-23 23:02:53 +00:00
|
|
|
:filter="[
|
|
|
|
|
'==',
|
|
|
|
|
['zoom'],
|
|
|
|
|
['+', 2, ['to-number', ['get', 'resolution']]],
|
|
|
|
|
]"
|
2026-04-24 13:23:03 +00:00
|
|
|
id="mosquito_source"
|
2026-04-24 00:31:03 +00:00
|
|
|
@mouseenter="doLayerMouseEnter()"
|
|
|
|
|
@mouseleave="doLayerMouseLeave()"
|
2026-04-23 23:02:53 +00:00
|
|
|
:paint="{ 'fill-opacity': 0.4, 'fill-color': '#dc3545' }"
|
|
|
|
|
source="tegola"
|
|
|
|
|
sourceLayer="mosquito_source"
|
|
|
|
|
type="fill"
|
|
|
|
|
/>
|
2026-04-23 23:46:31 +00:00
|
|
|
<Layer
|
2026-04-24 13:23:03 +00:00
|
|
|
@click="doClickMap"
|
2026-04-23 23:46:31 +00:00
|
|
|
id="parcel"
|
|
|
|
|
:minzoom="14"
|
|
|
|
|
:paint="{ 'line-color': '#0f0' }"
|
|
|
|
|
source="tegola"
|
|
|
|
|
sourceLayer="parcel"
|
|
|
|
|
type="line"
|
|
|
|
|
/>
|
2026-04-23 23:02:53 +00:00
|
|
|
<Layer
|
2026-04-24 13:23:03 +00:00
|
|
|
@click="doClickMap"
|
2026-04-23 23:02:53 +00:00
|
|
|
id="service_request"
|
|
|
|
|
:filter="[
|
|
|
|
|
'==',
|
|
|
|
|
['zoom'],
|
|
|
|
|
['+', 2, ['to-number', ['get', 'resolution']]],
|
|
|
|
|
]"
|
|
|
|
|
:paint="{ 'fill-opacity': 0.4, 'fill-color': '#ffc107' }"
|
|
|
|
|
source="tegola"
|
|
|
|
|
sourceLayer="service_request"
|
|
|
|
|
type="fill"
|
|
|
|
|
/>
|
|
|
|
|
<Layer
|
|
|
|
|
id="trap"
|
|
|
|
|
:filter="[
|
|
|
|
|
'==',
|
|
|
|
|
['zoom'],
|
|
|
|
|
['+', 2, ['to-number', ['get', 'resolution']]],
|
|
|
|
|
]"
|
|
|
|
|
:paint="{ 'fill-opacity': 0.4, 'fill-color': '#ffc107' }"
|
|
|
|
|
source="tegola"
|
|
|
|
|
sourceLayer="trap"
|
|
|
|
|
type="fill"
|
|
|
|
|
/>
|
|
|
|
|
<Layer
|
|
|
|
|
id="service-area"
|
|
|
|
|
:paint="{ 'line-color': '#f00' }"
|
|
|
|
|
source="tegola"
|
|
|
|
|
sourceLayer="service-area-bounds"
|
|
|
|
|
type="line"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Source
|
|
|
|
|
id="tegola"
|
|
|
|
|
type="vector"
|
|
|
|
|
:tiles="[
|
|
|
|
|
session.urls?.tegola +
|
|
|
|
|
'maps/nidus/{z}/{x}/{y}?id=' +
|
|
|
|
|
session.organization?.id,
|
|
|
|
|
]"
|
|
|
|
|
/>
|
|
|
|
|
</Map>
|
|
|
|
|
</div>
|
2026-04-16 17:14:57 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Recent Activity Section -->
|
|
|
|
|
<h3 class="section-title">Recent Activity</h3>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-12">
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-hover">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Date</th>
|
|
|
|
|
<th>Type</th>
|
|
|
|
|
<th>Location</th>
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
<th>Action</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-24 00:36:18 +00:00
|
|
|
import { onMounted, reactive, ref } from "vue";
|
2026-04-23 23:02:53 +00:00
|
|
|
import Map from "@/map/Map.vue";
|
2026-04-24 13:23:03 +00:00
|
|
|
import Layer, { MouseEvent } from "@/map/Layer.vue";
|
2026-04-23 23:02:53 +00:00
|
|
|
import Source from "@/map/Source.vue";
|
2026-04-23 23:38:12 +00:00
|
|
|
import { boundsDefault, boundsFromAPI } from "@/map/util";
|
2026-04-16 17:14:57 +00:00
|
|
|
import { formatBigNumber, formatTimeRelative } from "@/format";
|
2026-04-22 15:46:02 +00:00
|
|
|
import { router } from "@/route/config";
|
2026-04-24 13:48:00 +00:00
|
|
|
import { useRoutes } from "@/route/use";
|
2026-04-16 17:14:57 +00:00
|
|
|
import { useSessionStore } from "@/store/session";
|
|
|
|
|
import { useStoreServiceRequest } from "@/store/service_request";
|
|
|
|
|
import { useStoreSync } from "@/store/sync";
|
|
|
|
|
import type { Bounds } from "@/type/api";
|
|
|
|
|
|
|
|
|
|
const dashboard = reactive({
|
|
|
|
|
counts: {
|
|
|
|
|
service_requests: 0,
|
|
|
|
|
mosquito_sources: 0,
|
|
|
|
|
traps: 0,
|
|
|
|
|
},
|
|
|
|
|
organization: {
|
|
|
|
|
name: "",
|
|
|
|
|
id: 0,
|
|
|
|
|
},
|
|
|
|
|
isSyncOngoing: false,
|
|
|
|
|
lastSync: new Date(),
|
|
|
|
|
tegolaUrl: "",
|
|
|
|
|
serviceArea: {
|
|
|
|
|
min: { x: 0, y: 0 },
|
|
|
|
|
max: { x: 0, y: 0 },
|
|
|
|
|
},
|
|
|
|
|
recentRequests: [],
|
|
|
|
|
});
|
2026-04-24 00:36:18 +00:00
|
|
|
const mapCursor = ref<string>("");
|
2026-04-24 13:48:00 +00:00
|
|
|
const routes = useRoutes();
|
2026-04-16 17:14:57 +00:00
|
|
|
const storeServiceRequest = useStoreServiceRequest();
|
|
|
|
|
const storeSync = useStoreSync();
|
|
|
|
|
const session = useSessionStore();
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
const service_requests = await storeServiceRequest.fetchAll();
|
|
|
|
|
const syncs = await storeSync.fetchAll();
|
|
|
|
|
console.log("syncs", syncs);
|
|
|
|
|
});
|
2026-04-24 13:23:03 +00:00
|
|
|
function doClickMap(e: MouseEvent) {
|
|
|
|
|
if (!e.features || e.features.length == 0) return;
|
|
|
|
|
const feature = e.features[0];
|
|
|
|
|
const properties = feature.properties;
|
2026-04-24 13:48:00 +00:00
|
|
|
router.push(routes.CellDetail(properties.cell));
|
2026-04-22 15:46:02 +00:00
|
|
|
}
|
2026-04-24 00:31:03 +00:00
|
|
|
function doLayerMouseEnter() {
|
2026-04-24 00:36:18 +00:00
|
|
|
mapCursor.value = "pointer";
|
2026-04-24 00:31:03 +00:00
|
|
|
}
|
|
|
|
|
function doLayerMouseLeave() {
|
2026-04-24 00:36:18 +00:00
|
|
|
mapCursor.value = "";
|
2026-04-24 00:31:03 +00:00
|
|
|
}
|
2026-04-23 23:38:12 +00:00
|
|
|
function mapBounds(): maplibregl.LngLatBounds {
|
2026-04-16 17:14:57 +00:00
|
|
|
if (session.organization?.service_area) {
|
2026-04-23 23:38:12 +00:00
|
|
|
return boundsFromAPI(session.organization?.service_area);
|
2026-04-16 17:14:57 +00:00
|
|
|
}
|
2026-04-23 23:38:12 +00:00
|
|
|
return boundsDefault();
|
2026-04-16 17:14:57 +00:00
|
|
|
}
|
|
|
|
|
function refreshData() {
|
|
|
|
|
console.log("fake refresh");
|
|
|
|
|
}
|
|
|
|
|
</script>
|