Add loading indicator when checking for previous report data

This commit is contained in:
Eli Ribble 2026-04-10 15:38:31 +00:00
parent b23fc6edc5
commit 14c0d453e9
No known key found for this signature in database
4 changed files with 158 additions and 55 deletions

View file

@ -0,0 +1,75 @@
<template>
<div class="loading-wrapper position-relative">
<!-- Child content slot -->
<slot></slot>
<!-- Loading overlay -->
<div
v-if="isLoading"
class="loading-overlay position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
:class="overlayClass"
>
<div class="text-center">
<!-- Bootstrap spinner -->
<div
class="spinner-border text-primary"
:class="spinnerSize"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
<!-- Optional loading text -->
<div v-if="loadingText" class="mt-2">
<small class="text-muted">{{ loadingText }}</small>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LoadingOverlay",
props: {
isLoading: {
type: Boolean,
default: false,
},
loadingText: {
type: String,
default: "",
},
spinnerSize: {
type: String,
default: "",
validator: (value) => ["", "spinner-border-sm"].includes(value),
},
overlayOpacity: {
type: String,
default: "bg-light bg-opacity-75",
},
},
computed: {
overlayClass() {
return this.overlayOpacity;
},
},
};
</script>
<style scoped>
.loading-wrapper {
min-height: 50px; /* Ensure minimum height for overlay positioning */
}
.loading-overlay {
z-index: 1000;
backdrop-filter: blur(1px);
}
/* Ensure child content is not interactive when loading */
.loading-wrapper:has(.loading-overlay) > *:not(.loading-overlay) {
pointer-events: none;
user-select: none;
}
</style>

View file

@ -18,15 +18,20 @@ body > .container-fluid {
<template> <template>
<template v-if="district"> <template v-if="district">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<component <LoadingOverlay
:is="Component" :is-loading="isLoading"
:district="district" loading-text="Loading previous data"
@doEvidence="doEvidence" >
@doContact="doContact" <component
@doLocator="doLocator" :is="Component"
@doPermission="doPermission" :district="district"
v-model="compliance" @doEvidence="doEvidence"
/> @doContact="doContact"
@doLocator="doLocator"
@doPermission="doPermission"
v-model="compliance"
/>
</LoadingOverlay>
</router-view> </router-view>
</template> </template>
<template v-else> <template v-else>
@ -43,8 +48,9 @@ import { useStoreDistrict } from "@/rmo/store/district";
import { useStoreLocal } from "@/store/local"; import { useStoreLocal } from "@/store/local";
import { useStoreLocation } from "@/store/location"; import { useStoreLocation } from "@/store/location";
import Intro from "@/rmo/content/compliance/Intro.vue"; import Intro from "@/rmo/content/compliance/Intro.vue";
import type { District, Location, PublicReport } from "@/type/api"; import LoadingOverlay from "@/components/LoadingOverlay.vue";
import { PermissionAccess } from "@/type/api"; import type { District, PublicReport } from "@/type/api";
import { Address, Location, PermissionAccess } from "@/type/api";
import { Locator } from "@/type/map"; import { Locator } from "@/type/map";
import { type Contact } from "@/rmo/content/compliance/Contact.vue"; import { type Contact } from "@/rmo/content/compliance/Contact.vue";
import { type Permission } from "@/rmo/content/compliance/Permission.vue"; import { type Permission } from "@/rmo/content/compliance/Permission.vue";
@ -52,10 +58,12 @@ import { type Permission } from "@/rmo/content/compliance/Permission.vue";
export interface Compliance { export interface Compliance {
comments: string; comments: string;
contact: Contact; contact: Contact;
id: string;
images: Image[];
location: Location; location: Location;
locator: Locator; locator: Locator;
images: Image[];
permission: Permission; permission: Permission;
uri: string;
} }
interface Props { interface Props {
slug: string; slug: string;
@ -71,27 +79,15 @@ const compliance = ref<Compliance>({
can_text: true, can_text: true,
email: "", email: "",
}, },
id: "",
images: [], images: [],
location: { location: {
latitude: 0, latitude: 0,
longitude: 0, longitude: 0,
}, },
locator: { locator: {
address: { address: new Address(),
country: "", location: new Location(),
gid: "",
locality: "",
number: "",
postal_code: "",
raw: "",
region: "",
street: "",
unit: "",
},
location: {
latitude: 0,
longitude: 0,
},
}, },
permission: { permission: {
access: PermissionAccess.UNSELECTED, access: PermissionAccess.UNSELECTED,
@ -101,7 +97,9 @@ const compliance = ref<Compliance>({
has_dog: false, has_dog: false,
wants_scheduled: false, wants_scheduled: false,
}, },
uri: "",
}); });
const isLoading = ref<boolean>(true);
const props = defineProps<Props>(); const props = defineProps<Props>();
const report = ref<PublicReport | null>(); const report = ref<PublicReport | null>();
const district = computedAsync(async (): Promise<District | undefined> => { const district = computedAsync(async (): Promise<District | undefined> => {
@ -110,21 +108,18 @@ const district = computedAsync(async (): Promise<District | undefined> => {
}); });
const storeLocal = useStoreLocal(); const storeLocal = useStoreLocal();
const storeLocation = useStoreLocation(); const storeLocation = useStoreLocation();
function doEvidence() { async function createReport(client_id: string, loc?: GeolocationPosition) {
console.log("evidence", compliance.value);
}
function doContact() {
console.log("contact", compliance.value.contact);
}
function doLocator() {
console.log("locator done", compliance.value.locator);
}
function doPermission() {
console.log("permission", compliance.value.permission);
}
async function createReport(report_id: string, loc?: GeolocationPosition) {
const formData = new FormData(); const formData = new FormData();
formData.append; formData.append("client_id", client_id);
if (loc) {
formData.append("location.accuracy", loc.coords.accuracy.toString());
formData.append("location.latitude", loc.coords.latitude.toString());
formData.append("location.longitude", loc.coords.longitude.toString());
} else {
formData.append("location.accuracy", "0");
formData.append("location.latitude", "0");
formData.append("longitude", "0");
}
const resp = await fetch("/api/rmo/compliance", { const resp = await fetch("/api/rmo/compliance", {
method: "POST", method: "POST",
body: formData, body: formData,
@ -133,19 +128,52 @@ async function createReport(report_id: string, loc?: GeolocationPosition) {
if (!resp.ok) { if (!resp.ok) {
const content = await resp.text(); const content = await resp.text();
console.error("Failed to create compliance report", resp.status, content); console.error("Failed to create compliance report", resp.status, content);
return;
}
}
function doEvidence() {
console.log("evidence", compliance.value);
}
function doContact() {
console.log("contact", compliance.value.contact);
}
function doLocator() {
console.log("locator done", compliance.value.locator);
updateReport({
locator: compliance.value.locator,
});
}
function doPermission() {
console.log("permission", compliance.value.permission);
}
interface ComplianceUpdate {
locator?: Locator;
}
async function updateReport(updates: ComplianceUpdate) {
const resp = await fetch(compliance.value.uri, {
method: "PUT",
body: JSON.stringify(updates),
headers: {
"Content-Type": "application/json",
},
});
if (!resp.ok) {
const content = await resp.text();
console.error("Failed to update compliance", resp.status, content);
return;
} }
} }
onMounted(() => { onMounted(() => {
const session_id = storeLocal.getSessionID(); const client_id = storeLocal.getClientID();
storeLocation storeLocation
.get() .get()
.then((loc: GeolocationPosition) => { .then((loc: GeolocationPosition) => {
compliance.value.location = loc.coords; compliance.value.location = loc.coords;
createReport(session_id, loc); createReport(client_id, loc);
}) })
.catch((e) => { .catch((e) => {
console.log("failed to get location", e); console.log("failed to get location", e);
createReport(session_id); createReport(client_id);
}); });
}); });
</script> </script>

View file

@ -1,7 +1,7 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export const useStoreLocal = defineStore("local", () => { export const useStoreLocal = defineStore("local", () => {
function getSessionID(): string { function getClientID(): string {
let id = localStorage.getItem("session_id"); let id = localStorage.getItem("session_id");
if (id) { if (id) {
return id; return id;
@ -11,6 +11,6 @@ export const useStoreLocal = defineStore("local", () => {
return id; return id;
} }
return { return {
getSessionID, getClientID,
}; };
}); });

View file

@ -6,15 +6,15 @@ export enum PermissionAccess {
} }
export class Address { export class Address {
constructor( constructor(
public country: string, public country: string = "",
public gid: string, public gid: string = "",
public locality: string, public locality: string = "",
public number: string, public number: string = "",
public postal_code: string, public postal_code: string = "",
public raw: string, public raw: string = "",
public region: string, public region: string = "",
public street: string, public street: string = "",
public unit: string, public unit: string = "",
) {} ) {}
} }
export interface Bounds { export interface Bounds {