Set up dashboard page through VueJS

This commit is contained in:
Eli Ribble 2026-03-21 23:42:23 +00:00
parent bf3204992e
commit 6422609150
No known key found for this signature in database
9 changed files with 339 additions and 15 deletions

View file

@ -11,9 +11,6 @@
</button>
<ul class="sidebar-menu">
<li>
<RouterLink to="/about">About</RouterLink>
</li>
<li>
<NavigationLink to="/" icon="house" label="Home" />
</li>

View file

@ -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");

View file

@ -10,11 +10,6 @@ const routes: RouteRecordRaw[] = [
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: About,
},
{
path: "/intelligence",
name: "Intelligence",

47
ts/style/dashboard.scss Normal file
View file

@ -0,0 +1,47 @@
body {
background-color: #f8f9fa;
}
.stats-card {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
height: 100%;
}
.stats-card:hover {
transform: translateY(-5px);
}
.section-title {
margin: 30px 0 20px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.last-refreshed {
color: #6c757d;
}
.logo-placeholder {
width: 100px;
height: 40px;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.metric-icon {
font-size: 2rem;
margin-bottom: 10px;
display: inline-block;
width: 50px;
height: 50px;
line-height: 50px;
text-align: center;
border-radius: 50%;
}
.metric-value {
font-size: 2rem;
font-weight: bold;
}
.syncing {
color: #28a745;
animation: fa-spin 2s linear infinite;
}

View file

@ -67,4 +67,5 @@ i.bi svg {
// Import Bootstrap Icons
//@import "bootstrap-icons/font/bootstrap-icons.scss";
@import "./dashboard.scss";
@import "./sidebar.scss";

View file

@ -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>