Start adding other views and our initial stores
This commit is contained in:
parent
c75c5446f7
commit
736c71eefc
9 changed files with 332 additions and 458 deletions
|
|
@ -5,424 +5,4 @@
|
|||
<script src="/static/js/user-selector.js"></script>
|
||||
{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="container mt-4">
|
||||
<h2 id="comms-testing">
|
||||
<i class="bi bi-broadcast-pin"></i> Communications Testing
|
||||
</h2>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<!-- SMS Testing -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<i class="bi bi-chat-dots"></i> SMS Testing
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/sudo/sms" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="smsPhone" class="form-label"
|
||||
>Recipient Phone Number</label
|
||||
>
|
||||
<input
|
||||
type="tel"
|
||||
class="form-control"
|
||||
id="smsPhone"
|
||||
name="smsPhone"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smsMessage" class="form-label">Message</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="smsMessage"
|
||||
name="smsMessage"
|
||||
rows="3"
|
||||
maxlength="130"
|
||||
placeholder="Enter your SMS message here"
|
||||
></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Send SMS <i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MMS Testing -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<i class="bi bi-image"></i> MMS Testing
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="mmsPhone" class="form-label"
|
||||
>Recipient Phone Number</label
|
||||
>
|
||||
<input
|
||||
type="tel"
|
||||
class="form-control"
|
||||
id="mmsPhone"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="mmsMessage" class="form-label">Message</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="mmsMessage"
|
||||
rows="2"
|
||||
placeholder="Enter your MMS message here"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="mmsImage" class="form-label">Attach Image</label>
|
||||
<input
|
||||
class="form-control"
|
||||
type="file"
|
||||
id="mmsImage"
|
||||
accept="image/*"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">
|
||||
Send MMS <i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<!-- RCS Testing -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-info text-white">
|
||||
<i class="bi bi-chat-square-text"></i> RCS Testing
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="rcsPhone" class="form-label"
|
||||
>Recipient Phone Number</label
|
||||
>
|
||||
<input
|
||||
type="tel"
|
||||
class="form-control"
|
||||
id="rcsPhone"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rcsMessage" class="form-label">Message</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="rcsMessage"
|
||||
rows="3"
|
||||
placeholder="Enter your RCS message here"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Interactive Options</label>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="rcsIncludeButtons"
|
||||
/>
|
||||
<label class="form-check-label" for="rcsIncludeButtons"
|
||||
>Include Action Buttons</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-info">
|
||||
Send RCS <i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Testing -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<i class="bi bi-envelope"></i> Email Testing
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/sudo/email" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="emailFrom" class="form-label">From Account</label>
|
||||
<select class="form-select" id="emailFrom" name="emailFrom">
|
||||
<option value="{{ .C.ForwardEmailRMOAddress }}">
|
||||
{{ .C.ForwardEmailRMOAddress }}
|
||||
</option>
|
||||
<option value="{{ .C.ForwardEmailNidusAddress }}">
|
||||
{{ .C.ForwardEmailNidusAddress }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="emailTo" class="form-label">Recipient Email</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="emailTo"
|
||||
name="emailTo"
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="emailSubject" class="form-label">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="emailSubject"
|
||||
name="emailSubject"
|
||||
placeholder="Email Subject"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="emailBody" class="form-label">Message</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="emailBody"
|
||||
name="emailBody"
|
||||
placeholder="Enter your email message here"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning text-dark">
|
||||
Send Email <i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSE Testing -->
|
||||
<div class="card mb-5">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<i class="bi bi-bell"></i> Server-sent event testing
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/sudo/sse" method="POST">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="organizationID" class="form-label"
|
||||
>Organization ID</label
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
id="organization-id"
|
||||
name="organizationID"
|
||||
placeholder="Organization ID"
|
||||
type="text"
|
||||
value="{{ .Organization.ID }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Type</label>
|
||||
<select class="form-select" id="type" name="type">
|
||||
<option value="created">Created</option>
|
||||
<option value="deleted">Deleted</option>
|
||||
<option value="heartbeat">Heartbeat</option>
|
||||
<option value="sudo">Sudo</option>
|
||||
<option value="updated">Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="resource" class="form-label">Resource</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="resource"
|
||||
name="resource"
|
||||
type="text"
|
||||
value="rmo:nuisance"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="uriPath" class="form-label">URI path</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="uri-path"
|
||||
name="uriPath"
|
||||
type="text"
|
||||
value="/report/abcd-1234"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
Send SSE<i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5" />
|
||||
<!-- Push Notification Testing -->
|
||||
<div class="card mb-5">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<i class="bi bi-bell"></i> Push Notification Testing
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="pushUser" class="form-label"
|
||||
>User ID or Device Token</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="pushUser"
|
||||
placeholder="User ID or Device Token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="pushTitle" class="form-label"
|
||||
>Notification Title</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="pushTitle"
|
||||
placeholder="Notification Title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pushBody" class="form-label">Notification Body</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="pushBody"
|
||||
rows="2"
|
||||
placeholder="Enter your notification message here"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pushData" class="form-label"
|
||||
>Additional Data (JSON)</label
|
||||
>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="pushData"
|
||||
rows="2"
|
||||
placeholder='{"key": "value"}'
|
||||
></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
Send Push Notification <i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5" />
|
||||
|
||||
<!-- User Impersonation Section -->
|
||||
<h2 id="user-impersonation">
|
||||
<i class="bi bi-person-badge"></i> User Impersonation
|
||||
</h2>
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="bi bi-people"></i> Impersonate User
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="userSearch" class="form-label">Search Users</label>
|
||||
<user-selector></user-selector>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="userRole" class="form-label">Filter by Role</label>
|
||||
<select class="form-select" id="userRole">
|
||||
<option value="">All Roles</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">Standard User</option>
|
||||
<option value="support">Support</option>
|
||||
<option value="premium">Premium User</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User ID</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1001</td>
|
||||
<td>John Doe</td>
|
||||
<td>john.doe@example.com</td>
|
||||
<td><span class="badge bg-primary">Admin</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary">
|
||||
Impersonate <i class="bi bi-box-arrow-in-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1002</td>
|
||||
<td>Jane Smith</td>
|
||||
<td>jane.smith@example.com</td>
|
||||
<td><span class="badge bg-info">Support</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary">
|
||||
Impersonate <i class="bi bi-box-arrow-in-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1003</td>
|
||||
<td>Robert Johnson</td>
|
||||
<td>robert@example.com</td>
|
||||
<td><span class="badge bg-success">Premium User</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary">
|
||||
Impersonate <i class="bi bi-box-arrow-in-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1004</td>
|
||||
<td>Maria Garcia</td>
|
||||
<td>maria@example.com</td>
|
||||
<td><span class="badge bg-secondary">Standard User</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary">
|
||||
Impersonate <i class="bi bi-box-arrow-in-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="alert alert-warning mt-3 mb-0"
|
||||
id="impersonationStatus"
|
||||
style="display: none;"
|
||||
>
|
||||
<i class="bi bi-exclamation-triangle"></i> You are currently
|
||||
impersonating <strong>John Doe</strong>
|
||||
<button class="btn btn-sm btn-warning float-end">
|
||||
Exit Impersonation <i class="bi bi-box-arrow-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ func Router() chi.Router {
|
|||
r.Route("/api", api.AddRoutes)
|
||||
|
||||
r.Method("GET", "/", authenticatedHandler(getRoot))
|
||||
r.Method("GET", "/communication", 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))
|
||||
r.Method("GET", "/configuration", authenticatedHandler(getConfigurationRoot))
|
||||
r.Method("GET", "/configuration/integration", authenticatedHandler(getConfigurationIntegration))
|
||||
r.Method("GET", "/configuration/integration/arcgis", authenticatedHandler(getConfigurationIntegrationArcgis))
|
||||
|
|
|
|||
12
ts/App.vue
12
ts/App.vue
|
|
@ -2,15 +2,25 @@
|
|||
<div class="app-container">
|
||||
<Sidebar />
|
||||
<MainContent>
|
||||
<router-view />
|
||||
<div v-if="userStore.loading">Loading...</div>
|
||||
<div v-else-if="userStore.error">Error: {{ userStore.error }}</div>
|
||||
<router-view v-else />
|
||||
</MainContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useUserStore } from "@/store/user";
|
||||
|
||||
import Sidebar from "./components/layout/Sidebar.vue";
|
||||
import MainContent from "./components/layout/MainContent.vue";
|
||||
import NavigationLink from "./components/layout/common/NavigationLink.vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
onMounted(() => {
|
||||
userStore.fetchUser();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
180
ts/components/MapAggregate.vue
Normal file
180
ts/components/MapAggregate.vue
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
interface Props {
|
||||
centroid: [number, number];
|
||||
organizationId: number;
|
||||
tegola: string;
|
||||
xmin: number;
|
||||
ymin: number;
|
||||
xmax: number;
|
||||
ymax: number;
|
||||
}
|
||||
|
||||
interface CellClickDetail {
|
||||
cell: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
organizationId: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"cell-click": [detail: CellClickDetail];
|
||||
}>();
|
||||
|
||||
const mapContainer = ref<HTMLElement | null>(null);
|
||||
const map = ref<maplibregl.Map | null>(null);
|
||||
|
||||
const initializeMap = () => {
|
||||
if (!mapContainer.value) return;
|
||||
|
||||
const bounds: [[number, number], [number, number]] = [
|
||||
[props.xmin, props.ymin],
|
||||
[props.xmax, props.ymax],
|
||||
];
|
||||
|
||||
map.value = new maplibregl.Map({
|
||||
bounds: bounds,
|
||||
container: mapContainer.value,
|
||||
style: "https://tiles.stadiamaps.com/styles/alidade_smooth.json",
|
||||
});
|
||||
|
||||
console.log("Initializing map to bounds", bounds);
|
||||
|
||||
map.value.on("load", () => {
|
||||
if (!map.value) return;
|
||||
|
||||
map.value.addSource("tegola", {
|
||||
type: "vector",
|
||||
tiles: [
|
||||
`${props.tegola}maps/nidus/{z}/{x}/{y}?id=${props.organizationId}&organization_id=${props.organizationId}`,
|
||||
],
|
||||
});
|
||||
|
||||
map.value.addLayer({
|
||||
id: "mosquito_source",
|
||||
type: "fill",
|
||||
filter: ["==", ["zoom"], ["+", 2, ["to-number", ["get", "resolution"]]]],
|
||||
source: "tegola",
|
||||
"source-layer": "mosquito_source",
|
||||
paint: {
|
||||
"fill-opacity": 0.4,
|
||||
"fill-color": "#dc3545",
|
||||
},
|
||||
});
|
||||
|
||||
map.value.addLayer({
|
||||
id: "service_request",
|
||||
type: "fill",
|
||||
filter: ["==", ["zoom"], ["+", 2, ["to-number", ["get", "resolution"]]]],
|
||||
source: "tegola",
|
||||
"source-layer": "service_request",
|
||||
paint: {
|
||||
"fill-opacity": 0.4,
|
||||
"fill-color": "#ffc107",
|
||||
},
|
||||
});
|
||||
|
||||
map.value.addLayer({
|
||||
id: "trap",
|
||||
type: "fill",
|
||||
filter: ["==", ["zoom"], ["+", 2, ["to-number", ["get", "resolution"]]]],
|
||||
source: "tegola",
|
||||
"source-layer": "trap",
|
||||
paint: {
|
||||
"fill-opacity": 0.4,
|
||||
"fill-color": "#0dcaf0",
|
||||
},
|
||||
});
|
||||
|
||||
map.value.addLayer({
|
||||
id: "service-area",
|
||||
source: "tegola",
|
||||
"source-layer": "service-area-bounds",
|
||||
type: "line",
|
||||
paint: {
|
||||
"line-color": "#f00",
|
||||
},
|
||||
});
|
||||
|
||||
map.value.on("mouseenter", "mosquito_source", () => {
|
||||
if (map.value) {
|
||||
map.value.getCanvas().style.cursor = "pointer";
|
||||
}
|
||||
});
|
||||
|
||||
map.value.on("mouseleave", "mosquito_source", () => {
|
||||
if (map.value) {
|
||||
map.value.getCanvas().style.cursor = "";
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = (e: maplibregl.MapLayerMouseEvent) => {
|
||||
if (!e.features || e.features.length === 0) return;
|
||||
|
||||
const feature = e.features[0];
|
||||
const properties = feature.properties;
|
||||
|
||||
emit("cell-click", {
|
||||
cell: properties.cell,
|
||||
});
|
||||
};
|
||||
|
||||
map.value.on("click", "mosquito_source", handleClick);
|
||||
map.value.on("click", "service_request", handleClick);
|
||||
map.value.on("click", "trap", handleClick);
|
||||
});
|
||||
};
|
||||
|
||||
const jumpTo = (args: maplibregl.JumpToOptions) => {
|
||||
if (map.value) {
|
||||
map.value.jumpTo(args);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => initializeMap(), 0);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (map.value) {
|
||||
map.value.remove();
|
||||
map.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Expose public methods
|
||||
defineExpose({
|
||||
jumpTo,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="map-container">
|
||||
<div ref="mapContainer" class="map"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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;
|
||||
}
|
||||
|
||||
.map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
36
ts/router.ts
36
ts/router.ts
|
|
@ -2,7 +2,13 @@ import { createRouter, createWebHistory } from "vue-router";
|
|||
import type { RouteRecordRaw } from "vue-router";
|
||||
import Home from "./view/Home.vue";
|
||||
import About from "./view/About.vue";
|
||||
import Communication from "./view/Communication.vue";
|
||||
import Configuration from "./view/Configuration.vue";
|
||||
import Intelligence from "./view/Intelligence.vue";
|
||||
import Operations from "./view/Operations.vue";
|
||||
import Planning from "./view/Planning.vue";
|
||||
import Review from "./view/Review.vue";
|
||||
import Sudo from "./view/Sudo.vue";
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
|
|
@ -10,11 +16,41 @@ const routes: RouteRecordRaw[] = [
|
|||
name: "Home",
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: "/communication",
|
||||
name: "Communication",
|
||||
component: Communication,
|
||||
},
|
||||
{
|
||||
path: "/configuration",
|
||||
name: "Configuration",
|
||||
component: Configuration,
|
||||
},
|
||||
{
|
||||
path: "/intelligence",
|
||||
name: "Intelligence",
|
||||
component: Intelligence,
|
||||
},
|
||||
{
|
||||
path: "/operations",
|
||||
name: "Operations",
|
||||
component: Operations,
|
||||
},
|
||||
{
|
||||
path: "/planning",
|
||||
name: "Planning",
|
||||
component: Planning,
|
||||
},
|
||||
{
|
||||
path: "/review",
|
||||
name: "Review",
|
||||
component: Review,
|
||||
},
|
||||
{
|
||||
path: "/sudo",
|
||||
name: "Sudo",
|
||||
component: Sudo,
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
|
|
|||
43
ts/store/communication.ts
Normal file
43
ts/store/communication.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
export const useCommunicationStore = defineStore("communication", () => {
|
||||
// State
|
||||
const communications = ref(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
// Actions
|
||||
async function fetchCommunications() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append("sort", "-created");
|
||||
if (typeFilter.value) params.append("type", typeFilter.value);
|
||||
|
||||
const response = await fetch(
|
||||
`$${apiBase.value}/communication?$${params}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
communications.value = data.communications;
|
||||
|
||||
// if we already had something selected, reset it using the new data
|
||||
if (selectedCommunication.value) {
|
||||
const matching = communications.value.filter((report) => {
|
||||
return report.id === selectedCommunication.value.id;
|
||||
});
|
||||
if (matching.length > 0) {
|
||||
selectedCommunication.value = matching[0];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error loading communications:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
50
ts/store/user.ts
Normal file
50
ts/store/user.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
// State
|
||||
const user = ref(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => user.value !== null);
|
||||
const userName = computed(() => user.value?.name ?? "");
|
||||
const organization = computed(() => user.value?.organization ?? "");
|
||||
|
||||
// Actions
|
||||
async function fetchUser() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/user/self");
|
||||
if (!response.ok) throw new Error("Failed to fetch user");
|
||||
|
||||
user.value = await response.json();
|
||||
} catch (e) {
|
||||
error.value = e.message;
|
||||
console.error("Error fetching user:", e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearUser() {
|
||||
user.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
// Getters
|
||||
isAuthenticated,
|
||||
userName,
|
||||
organization,
|
||||
// Actions
|
||||
fetchUser,
|
||||
clearUser,
|
||||
};
|
||||
});
|
||||
|
|
@ -133,8 +133,8 @@
|
|||
<MapMultipoint
|
||||
id="map"
|
||||
ref="mapRef"
|
||||
:organization-id="organizationId"
|
||||
:tegola="tegolaUrl"
|
||||
:organization-id="organization.id"
|
||||
:tegola="organization.urls.tegola"
|
||||
:xmin="serviceArea.min.x"
|
||||
:ymin="serviceArea.min.y"
|
||||
:xmax="serviceArea.max.x"
|
||||
|
|
@ -775,16 +775,19 @@
|
|||
<script setup>
|
||||
import { ref, computed, onMounted, nextTick } from "vue";
|
||||
import maplibregl from "maplibre-gl";
|
||||
// Import your custom components
|
||||
|
||||
import { useCommunicationStore } from "../store/communication";
|
||||
import { useUserStore } from "../store/user";
|
||||
import MapMultipoint from "../components/MapMultipoint.vue";
|
||||
import TimeRelative from "../components/TimeRelative.vue";
|
||||
|
||||
const user = useUserStore();
|
||||
onMounted(() => {
|
||||
communicationStore.fetchCommunications();
|
||||
});
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
organizationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tegolaUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -871,35 +874,6 @@ function formatDate(date) {
|
|||
return new Date(date).toLocaleString();
|
||||
}
|
||||
|
||||
async function fetchCommunications() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append("sort", "-created");
|
||||
if (typeFilter.value) params.append("type", typeFilter.value);
|
||||
|
||||
const response = await fetch(`$${apiBase.value}/communication?$${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
communications.value = data.communications;
|
||||
|
||||
// if we already had something selected, reset it using the new data
|
||||
if (selectedCommunication.value) {
|
||||
const matching = communications.value.filter((report) => {
|
||||
return report.id === selectedCommunication.value.id;
|
||||
});
|
||||
if (matching.length > 0) {
|
||||
selectedCommunication.value = matching[0];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error loading communications:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFromAPI() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from "vue";
|
||||
import MapAggregate from "../components/MapAggregate.vue";
|
||||
const dashboard = reactive({
|
||||
counts: {
|
||||
service_requests: 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue