Set up dashboard page through VueJS
This commit is contained in:
parent
bf3204992e
commit
6422609150
9 changed files with 339 additions and 15 deletions
|
|
@ -8,6 +8,7 @@
|
|||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"maplibre-gl": "^5.21.0",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^5.0.4"
|
||||
},
|
||||
|
|
|
|||
94
pnpm-lock.yaml
generated
94
pnpm-lock.yaml
generated
|
|
@ -20,12 +20,15 @@ importers:
|
|||
maplibre-gl:
|
||||
specifier: ^5.21.0
|
||||
version: 5.21.0
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
||||
vue:
|
||||
specifier: ^3.5.30
|
||||
version: 3.5.30(typescript@5.9.3)
|
||||
vue-router:
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.4(@vue/compiler-sfc@3.5.30)(vue@3.5.30(typescript@5.9.3))
|
||||
version: 5.0.4(@vue/compiler-sfc@3.5.30)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))(vue@3.5.30(typescript@5.9.3))
|
||||
devDependencies:
|
||||
esbuild:
|
||||
specifier: ^0.25.5
|
||||
|
|
@ -389,12 +392,21 @@ packages:
|
|||
'@vue/compiler-ssr@3.5.30':
|
||||
resolution: {integrity: sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==}
|
||||
|
||||
'@vue/devtools-api@7.7.9':
|
||||
resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
|
||||
|
||||
'@vue/devtools-api@8.1.0':
|
||||
resolution: {integrity: sha512-O44X57jjkLKbLEc4OgL/6fEPOOanRJU8kYpCE8qfKlV96RQZcdzrcLI5mxMuVRUeXhHKIHGhCpHacyCk0HyO4w==}
|
||||
|
||||
'@vue/devtools-kit@7.7.9':
|
||||
resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==}
|
||||
|
||||
'@vue/devtools-kit@8.1.0':
|
||||
resolution: {integrity: sha512-/NZlS4WtGIB54DA/z10gzk+n/V7zaqSzYZOVlg2CfdnpIKdB61bd7JDIMxf/zrtX41zod8E2/bbEBoW/d7x70Q==}
|
||||
|
||||
'@vue/devtools-shared@7.7.9':
|
||||
resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==}
|
||||
|
||||
'@vue/devtools-shared@8.1.0':
|
||||
resolution: {integrity: sha512-h8uCb4Qs8UT8VdTT5yjY6tOJ//qH7EpxToixR0xqejR55t5OdISIg7AJ7eBkhBs8iu1qG5gY3QQNN1DF1EelAA==}
|
||||
|
||||
|
|
@ -456,6 +468,10 @@ packages:
|
|||
confbox@0.2.4:
|
||||
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
|
||||
|
||||
copy-anything@4.0.5:
|
||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
|
|
@ -546,6 +562,10 @@ packages:
|
|||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-what@5.5.0:
|
||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -580,6 +600,9 @@ packages:
|
|||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
mlly@1.8.2:
|
||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||
|
||||
|
|
@ -607,6 +630,9 @@ packages:
|
|||
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==}
|
||||
hasBin: true
|
||||
|
||||
perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
|
||||
perfect-debounce@2.1.0:
|
||||
resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
|
||||
|
||||
|
|
@ -617,6 +643,15 @@ packages:
|
|||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pinia@3.0.4:
|
||||
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.5.0'
|
||||
vue: ^3.5.11
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
|
|
@ -655,6 +690,9 @@ packages:
|
|||
engines: {node: '>= 0.4'}
|
||||
hasBin: true
|
||||
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rw@1.3.3:
|
||||
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
|
||||
|
||||
|
|
@ -782,9 +820,17 @@ packages:
|
|||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
supercluster@8.0.1:
|
||||
resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
|
||||
|
||||
superjson@2.2.6:
|
||||
resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -1143,10 +1189,24 @@ snapshots:
|
|||
'@vue/compiler-dom': 3.5.30
|
||||
'@vue/shared': 3.5.30
|
||||
|
||||
'@vue/devtools-api@7.7.9':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.9
|
||||
|
||||
'@vue/devtools-api@8.1.0':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 8.1.0
|
||||
|
||||
'@vue/devtools-kit@7.7.9':
|
||||
dependencies:
|
||||
'@vue/devtools-shared': 7.7.9
|
||||
birpc: 2.9.0
|
||||
hookable: 5.5.3
|
||||
mitt: 3.0.1
|
||||
perfect-debounce: 1.0.0
|
||||
speakingurl: 14.0.1
|
||||
superjson: 2.2.6
|
||||
|
||||
'@vue/devtools-kit@8.1.0':
|
||||
dependencies:
|
||||
'@vue/devtools-shared': 8.1.0
|
||||
|
|
@ -1154,6 +1214,10 @@ snapshots:
|
|||
hookable: 5.5.3
|
||||
perfect-debounce: 2.1.0
|
||||
|
||||
'@vue/devtools-shared@7.7.9':
|
||||
dependencies:
|
||||
rfdc: 1.4.1
|
||||
|
||||
'@vue/devtools-shared@8.1.0': {}
|
||||
|
||||
'@vue/reactivity@3.5.30':
|
||||
|
|
@ -1214,6 +1278,10 @@ snapshots:
|
|||
|
||||
confbox@0.2.4: {}
|
||||
|
||||
copy-anything@4.0.5:
|
||||
dependencies:
|
||||
is-what: 5.5.0
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
|
|
@ -1300,6 +1368,8 @@ snapshots:
|
|||
is-extglob: 2.1.1
|
||||
optional: true
|
||||
|
||||
is-what@5.5.0: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-stringify-pretty-compact@4.0.0: {}
|
||||
|
|
@ -1346,6 +1416,8 @@ snapshots:
|
|||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mlly@1.8.2:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
|
@ -1370,12 +1442,21 @@ snapshots:
|
|||
dependencies:
|
||||
resolve-protobuf-schema: 2.1.0
|
||||
|
||||
perfect-debounce@1.0.0: {}
|
||||
|
||||
perfect-debounce@2.1.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.9
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
|
|
@ -1416,6 +1497,8 @@ snapshots:
|
|||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rw@1.3.3: {}
|
||||
|
||||
rxjs@7.8.2:
|
||||
|
|
@ -1521,10 +1604,16 @@ snapshots:
|
|||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
supercluster@8.0.1:
|
||||
dependencies:
|
||||
kdbush: 4.0.2
|
||||
|
||||
superjson@2.2.6:
|
||||
dependencies:
|
||||
copy-anything: 4.0.5
|
||||
|
||||
supports-color@8.1.1:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
|
@ -1565,7 +1654,7 @@ snapshots:
|
|||
|
||||
varint@6.0.0: {}
|
||||
|
||||
vue-router@5.0.4(@vue/compiler-sfc@3.5.30)(vue@3.5.30(typescript@5.9.3)):
|
||||
vue-router@5.0.4(@vue/compiler-sfc@3.5.30)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))(vue@3.5.30(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@babel/generator': 7.29.1
|
||||
'@vue-macros/common': 3.1.2(vue@3.5.30(typescript@5.9.3))
|
||||
|
|
@ -1587,6 +1676,7 @@ snapshots:
|
|||
yaml: 2.8.3
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.30
|
||||
pinia: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
||||
|
||||
vue@3.5.30(typescript@5.9.3):
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ func Router() chi.Router {
|
|||
r.Route("/api", api.AddRoutes)
|
||||
|
||||
r.Method("GET", "/", authenticatedHandler(getRoot))
|
||||
r.Method("GET", "/intelligence", authenticatedHandler(getRoot))
|
||||
|
||||
r.Method("GET", "/admin", authenticatedHandler(getAdminDash))
|
||||
r.Method("GET", "/cell/{cell}", authenticatedHandler(getCellDetails))
|
||||
r.Method("GET", "/communication", authenticatedHandler(getCommunicationRoot))
|
||||
|
|
@ -63,7 +65,6 @@ func Router() chi.Router {
|
|||
r.Method("GET", "/configuration/user", authenticatedHandler(getConfigurationUserList))
|
||||
r.Method("GET", "/configuration/user/add", authenticatedHandler(getConfigurationUserAdd))
|
||||
r.Method("GET", "/download", authenticatedHandler(getDownloadList))
|
||||
r.Method("GET", "/intelligence", authenticatedHandler(getIntelligenceRoot))
|
||||
r.Method("GET", "/layout-test", authenticatedHandler(getLayoutTest))
|
||||
r.Method("GET", "/message", authenticatedHandler(getMessageList))
|
||||
r.Method("GET", "/notification", authenticatedHandler(getNotificationList))
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@
|
|||
</button>
|
||||
|
||||
<ul class="sidebar-menu">
|
||||
<li>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationLink to="/" icon="house" label="Home" />
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Alpine from "./vendor/alpinejs-3.15.8.js";
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { SSEManager } from "./sse-manager";
|
||||
|
|
@ -53,6 +54,8 @@ interface GreetingComponent {
|
|||
updateMessage(): void;
|
||||
}
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
|
|
|
|||
|
|
@ -10,11 +10,6 @@ const routes: RouteRecordRaw[] = [
|
|||
name: "Home",
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "About",
|
||||
component: About,
|
||||
},
|
||||
{
|
||||
path: "/intelligence",
|
||||
name: "Intelligence",
|
||||
|
|
|
|||
|
|
@ -67,4 +67,5 @@ i.bi svg {
|
|||
// Import Bootstrap Icons
|
||||
//@import "bootstrap-icons/font/bootstrap-icons.scss";
|
||||
|
||||
@import "./dashboard.scss";
|
||||
@import "./sidebar.scss";
|
||||
|
|
|
|||
244
ts/view/Home.vue
244
ts/view/Home.vue
|
|
@ -1,10 +1,246 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>Welcome Home</h1>
|
||||
<p>This is the home page content.</p>
|
||||
<!-- Dashboard Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h1>{{ dashboard.organization.name }} Dashboard</h1>
|
||||
<p class="text-muted">
|
||||
Overview of mosquito control activities in your district
|
||||
</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">
|
||||
<p v-if="dashboard.serviceArea.min.x === 0.0">
|
||||
No service area for this organization yet
|
||||
</p>
|
||||
<map-aggregate
|
||||
v-else
|
||||
:organization-id="dashboard.organization.id"
|
||||
:tegola="dashboard.tegolaUrl"
|
||||
:xmin="dashboard.serviceArea.min.x"
|
||||
:ymin="dashboard.serviceArea.min.y"
|
||||
:xmax="dashboard.serviceArea.max.x"
|
||||
:ymax="dashboard.serviceArea.max.y"
|
||||
/>
|
||||
</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>
|
||||
<tbody>
|
||||
<tr v-for="(sr, i) in dashboard.recentRequests" :key="i">
|
||||
<td>{{ formatTimeRelative(sr.date) }}</td>
|
||||
<td>Service Request</td>
|
||||
<td>{{ sr.location }}</td>
|
||||
<td><span class="badge bg-success">Completed</span></td>
|
||||
<td>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Component logic here
|
||||
import { onMounted, reactive } from "vue";
|
||||
const dashboard = reactive({
|
||||
counts: {
|
||||
service_requests: 0,
|
||||
mosquito_sources: 0,
|
||||
traps: 0,
|
||||
},
|
||||
organization: {
|
||||
name: "",
|
||||
id: "",
|
||||
},
|
||||
isSyncOngoing: false,
|
||||
lastSync: new Date(),
|
||||
tegolaUrl: "",
|
||||
serviceArea: {
|
||||
min: { x: 0, y: 0 },
|
||||
max: { x: 0, y: 0 },
|
||||
},
|
||||
recentRequests: [],
|
||||
});
|
||||
onMounted(async () => {});
|
||||
function formatBigNumber(n: number): string {
|
||||
// Convert the number to a string
|
||||
const numStr = n.toString();
|
||||
|
||||
// Add commas every three digits from the right
|
||||
let result = "";
|
||||
for (let i = 0; i < numStr.length; i++) {
|
||||
if (i > 0 && (numStr.length - i) % 3 === 0) {
|
||||
result += ",";
|
||||
}
|
||||
result += numStr[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
function formatTimeRelative(t: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - t.getTime();
|
||||
|
||||
const hours = diffMs / (1000 * 60 * 60);
|
||||
|
||||
if (hours > 0) {
|
||||
if (hours < 1) {
|
||||
const minutes = diffMs / (1000 * 60);
|
||||
return `${Math.floor(minutes)} minutes ago`;
|
||||
} else if (hours < 24) {
|
||||
return `${Math.floor(hours)} hours ago`;
|
||||
} else {
|
||||
const days = hours / 24;
|
||||
return `${Math.floor(days)} days ago`;
|
||||
}
|
||||
} else {
|
||||
if (hours < -24) {
|
||||
const days = hours / 24;
|
||||
return `in ${Math.floor(-1 * days)} days`;
|
||||
} else if (hours < -1) {
|
||||
return `in ${Math.floor(-1 * hours)} hours`;
|
||||
} else {
|
||||
const minutes = diffMs / (1000 * 60);
|
||||
if (minutes > -1) {
|
||||
const seconds = diffMs / 1000;
|
||||
return `in ${Math.floor(-1 * seconds)} seconds`;
|
||||
}
|
||||
return `in ${Math.floor(-1 * minutes)} minutes`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function refreshData() {
|
||||
console.log("fake refresh");
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue