Switch address to contain an embedded location, start saving compliance

This commit is contained in:
Eli Ribble 2026-04-10 16:59:29 +00:00
parent 14c0d453e9
commit bac55774f8
No known key found for this signature in database
16 changed files with 281 additions and 238 deletions

View file

@ -38,7 +38,7 @@
<template>
<div class="mb-4">
<AddressSuggestion
v-model="modelValue.address"
v-model="modelValue"
placeholder="Start typing an address (min 3 characters)"
@suggestion-selected="doAddressSuggestionSelected"
>
@ -63,17 +63,18 @@
import { computed, ref } from "vue";
import AddressSuggestion from "@/components/AddressSuggestion.vue";
import MapLocator from "@/components/MapLocator.vue";
import type { Address, Geocode, GeocodeSuggestion, Location } from "@/type/api";
import { Address } from "@/type/api";
import type { Geocode, GeocodeSuggestion, Location } from "@/type/api";
import { useGeocodeStore } from "@/store/geocode";
import { Camera, Locator } from "@/type/map";
import { Camera } from "@/type/map";
import type { Marker } from "@/types";
interface Emits {
(e: "update:modelValue", value: Locator): void;
(e: "update:modelValue", value: Address): void;
}
interface Props {
initialCamera?: Camera;
modelValue: Locator;
modelValue: Address;
}
const address = ref<string>("");
const currentCamera = ref<Camera>(new Camera());
@ -97,6 +98,10 @@ const markers = computed((): Marker[] => {
};
return [marker];
});
const modelValue = computed({
get: () => props.modelValue,
set: (value: Address) => emit("update:modelValue", value),
});
const props = defineProps<Props>();
function doAddressSuggestionSelected(suggestion: GeocodeSuggestion) {
console.log("Address suggestion selected", suggestion);
@ -121,11 +126,7 @@ async function doAddressSuggestionDetails(suggestion: GeocodeSuggestion) {
updateModel(data.address.gid, data.address.raw, data.location);
}
function doMapClick(location: Location) {
updateModel(
props.modelValue.address.gid,
props.modelValue.address.raw,
location,
);
updateModel(props.modelValue.gid, props.modelValue.raw, location);
geocode
.reverse(location)
.then((code: Geocode) => {
@ -141,31 +142,25 @@ function doMapClick(location: Location) {
});
}
function doMapMarkerDragEnd(location: Location) {
updateModel(
props.modelValue.address.gid,
props.modelValue.address.raw,
location,
);
updateModel(props.modelValue.gid, props.modelValue.raw, location);
}
function updateModel(
address_gid: string,
address_raw: string,
location: Location,
location?: Location,
) {
const newLocator: Locator = {
address: {
country: "",
gid: address_gid,
locality: "",
number: "",
postal_code: "",
raw: address_raw,
region: "",
street: "",
unit: "",
},
location: location,
};
emit("update:modelValue", newLocator);
const newAddress = new Address(
"",
address_gid,
"",
"",
"",
address_raw,
"",
"",
"",
location,
);
emit("update:modelValue", newAddress);
}
</script>

View file

@ -125,7 +125,7 @@ select.tall {
<p class="small text-muted mb-2">
You can also click on the map to mark the location precisely
</p>
<AddressAndMapLocator v-model="locator" />
<AddressAndMapLocator v-model="address" />
<!-- Mosquito Activity Section -->
<div class="form-section">
@ -472,15 +472,16 @@ import { useGeocodeStore } from "@/store/geocode";
import { useStoreLocation } from "@/store/location";
import { useStorePublicReport } from "@/store/publicreport";
import type { Marker } from "@/types";
import type {
import {
Address,
Geocode,
GeocodeSuggestion,
Location,
PublicReport,
type Geocode,
type GeocodeSuggestion,
type Location,
type PublicReport,
} from "@/type/api";
import type { Camera, Locator } from "@/type/map";
import type { Camera } from "@/type/map";
const address = ref<Address>(new Address());
const currentCamera = ref<Camera | null>(null);
const currentLocation = ref<Location | null>(null);
const errorMessage = ref("");
@ -488,23 +489,6 @@ const formElement = ref<HTMLFormElement | null>(null);
const images = ref<Image[]>([]);
const isSubmitting = ref(false);
const storeLocation = useStoreLocation();
const locator = ref<Locator>({
address: {
country: "",
gid: "",
locality: "",
number: "",
postal_code: "",
raw: "",
region: "",
street: "",
unit: "",
},
location: {
latitude: 0,
longitude: 0,
},
});
const showMore = ref<boolean>(false);
const storePublicReport = useStorePublicReport();
@ -517,19 +501,17 @@ async function doSubmit() {
errorMessage.value = "";
try {
const formData = new FormData(formElement.value);
if (locator.value) {
if (locator.value.address) {
formData.append("locator.address.gid", locator.value.address.gid);
formData.append("locator.address.raw", locator.value.address.raw);
}
if (locator.value.location) {
if (address.value) {
formData.append("address.gid", address.value.gid);
formData.append("address.raw", address.value.raw);
if (address.value.location) {
formData.append(
"locator.location.latitude",
locator.value.location.latitude.toString(),
"address.location.latitude",
address.value.location.latitude.toString(),
);
formData.append(
"locator.location.longitude",
locator.value.location.longitude.toString(),
"address.location.longitude",
address.value.location.longitude.toString(),
);
}
}

View file

@ -240,7 +240,7 @@ select.tall {
<p class="small text-muted mb-2">
You can also click on the map to mark the location precisely
</p>
<AddressAndMapLocator v-model="locator" />
<AddressAndMapLocator v-model="address" />
</div>
<button
@ -619,28 +619,22 @@ select.tall {
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import AddressSuggestion from "@/components/AddressSuggestion.vue";
import ImageUpload, { Image } from "@/components/ImageUpload.vue";
import MapLocator from "@/components/MapLocator.vue";
import Tooltip from "@/components/Tooltip.vue";
import { useGeocodeStore } from "@/store/geocode";
import { useStoreLocation } from "@/store/location";
import { useStorePublicReport } from "@/store/publicreport";
import type { Marker } from "@/types";
import type {
import {
Address,
Geocode,
GeocodeSuggestion,
Location,
PublicReport,
type Geocode,
type GeocodeSuggestion,
type Location,
type PublicReport,
} from "@/type/api";
import type { Camera, Locator } from "@/type/map";
import type { Camera } from "@/type/map";
const isCollapsed = ref<boolean>(true);
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value;
};
const address = ref<string>("");
const address = ref<Address>(new Address());
const currentCamera = ref<Camera | null>(null);
const currentLocation = ref<Location | null>(null);
const errorMessage = ref("");
@ -657,84 +651,9 @@ const markers = computed((): Marker[] => {
}
});
const storeLocation = useStoreLocation();
const locator = ref<Locator>({
address: {
country: "",
gid: "",
locality: "",
number: "",
postal_code: "",
raw: "",
region: "",
street: "",
unit: "",
},
location: {
latitude: 0,
longitude: 0,
},
});
const router = useRouter();
const selectedSuggestion = ref<GeocodeSuggestion | null>(null);
const showMore = ref<boolean>(false);
const storePublicReport = useStorePublicReport();
function doAddressSuggestionSelected(suggestion: GeocodeSuggestion) {
console.log("Address suggestion selected", suggestion);
doAddressSuggestionDetails(suggestion);
}
async function doAddressSuggestionDetails(suggestion: GeocodeSuggestion) {
// Fetch full details for the selected suggestion
selectedSuggestion.value = suggestion;
const url = `/api/geocode/by-gid/${suggestion.gid}`;
const response = await fetch(url);
if (!response.ok) {
console.error("Failed to get suggestion detail", response.statusText);
return;
}
const data = (await response.json()) as Geocode;
if (currentCamera.value) {
currentCamera.value.zoom = 15;
}
marker.value = {
color: "#FF0000",
draggable: true,
id: "x",
location: data.location,
};
}
function doMapClick(location: Location) {
marker.value = {
color: "#FF0000",
draggable: true,
id: "x",
location: location,
};
geocode
.reverse(location)
.then((code: Geocode) => {
address.value = code.address.raw;
selectedSuggestion.value = {
detail: code.address.number + " " + code.address.street,
gid: code.address.gid,
locality: code.address.locality,
type: "address",
};
console.log("reverse geocoded", code);
})
.catch((e) => {
console.error("failed to reverse geocode after map click", e);
});
}
function doMapMarkerDragEnd(location: Location) {
marker.value = {
color: "#FF0000",
draggable: true,
id: "x",
location: location,
};
}
async function doSubmit() {
if (!formElement.value) return;
@ -742,17 +661,35 @@ async function doSubmit() {
errorMessage.value = "";
try {
const formData = new FormData(formElement.value);
if (selectedSuggestion.value) {
formData.append("address-gid", selectedSuggestion.value.gid);
}
if (currentLocation.value) {
formData.append("latitude", currentLocation.value.latitude.toString());
formData.append("longitude", currentLocation.value.longitude.toString());
if (address.value) {
formData.append("address.gid", address.value.gid);
formData.append("address.raw", address.value.raw);
if (address.value.location) {
formData.append(
"address.location.latitude",
address.value.location.latitude.toString(),
);
formData.append(
"address.location.longitude",
address.value.location.longitude.toString(),
);
}
}
formData.append(
"location.accuracy",
currentLocation.value?.accuracy?.toString() ?? "0",
);
formData.append(
"location.latitude",
currentLocation.value?.latitude.toString() ?? "0",
);
formData.append(
"location.longitude",
currentLocation.value?.longitude.toString() ?? "0",
);
images.value.forEach((image, index) => {
formData.append(`image[${index}]`, image.file, image.name);
});
formData.append("address", address.value);
const resp = await fetch("/api/rmo/water", {
method: "POST",
body: formData,

View file

@ -12,7 +12,7 @@
<AddressAndMapLocator
:initialCamera="initialCamera"
v-model="modelValue.locator"
v-model="modelValue.address"
/>
<div class="d-flex gap-2 mt-4">
@ -35,10 +35,10 @@ import HeaderCompliance from "@/rmo/components/HeaderCompliance.vue";
import ProgressBarCompliance from "@/rmo/components/ProgressBarCompliance.vue";
import AddressAndMapLocator from "@/rmo/components/AddressAndMapLocator.vue";
import { Compliance } from "@/rmo/view/Compliance.vue";
import { Camera, Locator } from "@/type/map";
import { Camera } from "@/type/map";
interface Emits {
(e: "doLocator"): void;
(e: "doAddress"): void;
(e: "update:modelValue", value: Compliance): void;
}
interface Props {
@ -59,7 +59,7 @@ const initialCamera = computed((): Camera | undefined => {
});
function doContinue() {
emit("update:modelValue", props.modelValue);
emit("doLocator");
emit("doAddress");
// re-add when we have the concern data to show
// router.push("./concern");
router.push(`/district/${props.district.slug}/compliance/evidence`);

View file

@ -111,8 +111,8 @@
<div class="summary-section">
<h3><i class="bi bi-geo-alt"></i> Property Address</h3>
<div class="summary-item">
<div class="summary-value" v-if="modelValue.locator?.address.raw">
{{ modelValue.locator.address.raw }}
<div class="summary-value" v-if="modelValue.address.raw">
{{ modelValue.address.raw }}
<span class="status-badge status-provided ms-2">
<i class="bi bi-check-circle"></i> Provided
</span>

View file

@ -25,9 +25,9 @@ body > .container-fluid {
<component
:is="Component"
:district="district"
@doAddress="doAddress"
@doEvidence="doEvidence"
@doContact="doContact"
@doLocator="doLocator"
@doPermission="doPermission"
v-model="compliance"
/>
@ -51,20 +51,29 @@ import Intro from "@/rmo/content/compliance/Intro.vue";
import LoadingOverlay from "@/components/LoadingOverlay.vue";
import type { District, PublicReport } from "@/type/api";
import { Address, Location, PermissionAccess } from "@/type/api";
import { Locator } from "@/type/map";
import { type Contact } from "@/rmo/content/compliance/Contact.vue";
import { type Permission } from "@/rmo/content/compliance/Permission.vue";
export interface Compliance {
address: Address;
comments: string;
contact: Contact;
id: string;
images: Image[];
location: Location;
locator: Locator;
permission: Permission;
uri: string;
}
interface ComplianceUpdate {
address?: Address;
comments?: string;
contact?: Contact;
//id: string;
//images?: Image[];
location?: Location;
permission?: Permission;
//uri: string;
}
interface Props {
slug: string;
}
@ -72,6 +81,7 @@ interface Props {
const districtStore = useStoreDistrict();
const compliance = ref<Compliance>({
address: new Address(),
comments: "",
contact: {
name: "",
@ -85,10 +95,6 @@ const compliance = ref<Compliance>({
latitude: 0,
longitude: 0,
},
locator: {
address: new Address(),
location: new Location(),
},
permission: {
access: PermissionAccess.UNSELECTED,
access_instructions: "",
@ -130,6 +136,14 @@ async function createReport(client_id: string, loc?: GeolocationPosition) {
console.error("Failed to create compliance report", resp.status, content);
return;
}
const body = await resp.json();
storeLocal.setExistingComplianceReportURI(body.uri);
}
function doAddress() {
console.log("address done", compliance.value.address);
updateReport({
address: compliance.value.address,
});
}
function doEvidence() {
console.log("evidence", compliance.value);
@ -137,17 +151,28 @@ function doEvidence() {
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 fetchExistingReport(report_uri: string) {
isLoading.value = true;
const resp = await fetch(report_uri);
if (!resp.ok) {
isLoading.value = false;
const content = await resp.text();
console.error(
"Failed to fetch existing report",
report_uri,
resp.status,
content,
);
return;
}
const body = await resp.json();
compliance.value.id = body.id;
compliance.value.uri = body.uri;
compliance.value.address = body.address;
isLoading.value = false;
}
async function updateReport(updates: ComplianceUpdate) {
const resp = await fetch(compliance.value.uri, {
@ -165,15 +190,23 @@ async function updateReport(updates: ComplianceUpdate) {
}
onMounted(() => {
const client_id = storeLocal.getClientID();
const report_uri = storeLocal.getExistingComplianceReportURI();
if (report_uri) {
fetchExistingReport(report_uri);
} else {
isLoading.value = false;
createReport(client_id);
}
storeLocation
.get()
.then((loc: GeolocationPosition) => {
compliance.value.location = loc.coords;
createReport(client_id, loc);
updateReport({
location: compliance.value.location,
});
})
.catch((e) => {
console.log("failed to get location", e);
createReport(client_id);
});
});
</script>

View file

@ -10,7 +10,15 @@ export const useStoreLocal = defineStore("local", () => {
localStorage.setItem("session_id", id.toString());
return id;
}
function getExistingComplianceReportURI(): string | null {
return localStorage.getItem("working_compilance_report_uri");
}
function setExistingComplianceReportURI(uri: string) {
localStorage.setItem("working_compilance_report_uri", uri);
}
return {
getClientID,
getExistingComplianceReportURI,
setExistingComplianceReportURI,
};
});

View file

@ -15,6 +15,7 @@ export class Address {
public region: string = "",
public street: string = "",
public unit: string = "",
public location?: Location,
) {}
}
export interface Bounds {

View file

@ -9,12 +9,6 @@ export class Camera {
this.zoom = zoom;
}
}
export class Locator {
constructor(
public address: Address,
public location: Location,
) {}
}
export type MoveEndEventInternal = maplibregl.MapLibreEvent<
| maplibregl.MapMouseEvent
| maplibregl.MapTouchEvent