Switch address to contain an embedded location, start saving compliance
This commit is contained in:
parent
14c0d453e9
commit
bac55774f8
16 changed files with 281 additions and 238 deletions
|
|
@ -263,6 +263,29 @@ func handlerJSONPost[RequestType any, ResponseType any](f handlerFunctionPost[Re
|
|||
w.Write(body)
|
||||
}
|
||||
}
|
||||
|
||||
func handlerJSONPut[RequestType any, ResponseType any](f handlerFunctionPost[RequestType, ResponseType]) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
req, e := parseRequest[RequestType](r)
|
||||
if e != nil {
|
||||
serializeError(w, e)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
resp, e := f(ctx, r, *req)
|
||||
if e != nil {
|
||||
serializeError(w, e)
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
|
||||
return
|
||||
}
|
||||
w.Write(body)
|
||||
}
|
||||
}
|
||||
func handlerFormPost[RequestType any, ResponseType any](f handlerFunctionPostFormMultipart[RequestType, ResponseType]) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ func AddRoutes(r *mux.Router) {
|
|||
r.Handle("/review-task", authenticatedHandlerJSON(review_task.List)).Methods("GET")
|
||||
compliance := resource.Compliance(router)
|
||||
r.HandleFunc("/rmo/compliance", handlerFormPost(compliance.Create)).Methods("POST")
|
||||
r.HandleFunc("/rmo/compliance/{public_id}", handlerFormPost(compliance.Update)).Methods("PUT")
|
||||
nuisance := resource.Nuisance(router)
|
||||
r.HandleFunc("/rmo/nuisance", handlerFormPost(nuisance.Create)).Methods("POST")
|
||||
water := resource.Water(router)
|
||||
|
|
@ -83,6 +82,7 @@ func AddRoutes(r *mux.Router) {
|
|||
r.Handle("/geocode/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET")
|
||||
publicreport := resource.Publicreport(router)
|
||||
r.Handle("/publicreport/{id}", handlerJSON(publicreport.ByID)).Methods("GET").Name("publicreport.ByIDGet")
|
||||
r.Handle("/publicreport/{id}", handlerJSONPut(publicreport.Update)).Methods("PUT")
|
||||
publicreport_notification := resource.PublicreportNotification(router)
|
||||
r.Handle("/publicreport-notification", handlerJSONPost(publicreport_notification.Create)).Methods("POST")
|
||||
|
||||
|
|
|
|||
|
|
@ -29,10 +29,13 @@ func PublicreportByID(ctx context.Context, report_id string) (*types.Report, err
|
|||
return publicreport.Report(ctx, report_id)
|
||||
}
|
||||
func PublicreportInvalid(ctx context.Context, user User, report_id string) error {
|
||||
report, err := reportFromID(ctx, user, report_id)
|
||||
report, err := reportFromID(ctx, report_id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query report existence: %w", err)
|
||||
}
|
||||
if report.OrganizationID != user.Organization.ID {
|
||||
return fmt.Errorf("user is from a different organization")
|
||||
}
|
||||
|
||||
err = report.Update(ctx, db.PGInstance.BobDB, &models.PublicreportReportSetter{
|
||||
Reviewed: omitnull.From(time.Now()),
|
||||
|
|
@ -52,10 +55,13 @@ func PublicReportMessageCreate(ctx context.Context, user User, report_id, messag
|
|||
}
|
||||
defer txn.Rollback(ctx)
|
||||
|
||||
report, err := reportFromID(ctx, user, report_id)
|
||||
report, err := reportFromID(ctx, report_id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query report existence: %w", err)
|
||||
}
|
||||
if report.OrganizationID != user.Organization.ID {
|
||||
return nil, fmt.Errorf("user is from a different organization")
|
||||
}
|
||||
if report.ReporterPhone != "" {
|
||||
log.Debug().Str("report_id", report_id).Msg("contacting via phone")
|
||||
p, err := text.ParsePhoneNumber(report.ReporterPhone)
|
||||
|
|
@ -81,6 +87,29 @@ func PublicReportMessageCreate(ctx context.Context, user User, report_id, messag
|
|||
return nil, errors.New("no contact methods available")
|
||||
}
|
||||
}
|
||||
func PublicReportUpdate(ctx context.Context, report_id string, report_setter models.PublicreportReportSetter, location *types.Location) (*types.Report, error) {
|
||||
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create txn: %w", err)
|
||||
}
|
||||
defer txn.Rollback(ctx)
|
||||
report, err := reportFromID(ctx, report_id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query report existence: %w", err)
|
||||
}
|
||||
err = report.Update(ctx, txn, &report_setter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update report: %w", err)
|
||||
}
|
||||
if location != nil {
|
||||
err = reportUpdateLocation(ctx, txn, report.ID, *location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update location: %w", err)
|
||||
}
|
||||
}
|
||||
txn.Commit(ctx)
|
||||
return publicreport.Report(ctx, report_id)
|
||||
}
|
||||
func PublicReportReporterUpdated(ctx context.Context, org_id int32, report_id string) {
|
||||
event.Updated(event.TypeRMOReport, org_id, report_id)
|
||||
}
|
||||
|
|
@ -170,17 +199,7 @@ func reportCreate(ctx context.Context, setter_report models.PublicreportReportSe
|
|||
if location != nil {
|
||||
l := *location
|
||||
if l.Latitude != 0 && l.Longitude != 0 {
|
||||
h3cell, _ := location.H3Cell()
|
||||
geom_query, _ := location.GeometryQuery()
|
||||
_, err = psql.Update(
|
||||
um.Table("publicreport.report"),
|
||||
um.SetCol("h3cell").ToArg(h3cell),
|
||||
um.SetCol("location").To(geom_query),
|
||||
um.Where(psql.Quote("id").EQ(psql.Arg(result.ID))),
|
||||
).Exec(ctx, txn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to insert publicreport.report geospatial", err)
|
||||
}
|
||||
reportUpdateLocation(ctx, txn, result.ID, l)
|
||||
}
|
||||
}
|
||||
log.Info().Str("public_id", public_id).Int32("id", result.ID).Msg("Created base report")
|
||||
|
|
@ -226,13 +245,26 @@ func reportCreate(ctx context.Context, setter_report models.PublicreportReportSe
|
|||
}
|
||||
return result, nil
|
||||
}
|
||||
func reportFromID(ctx context.Context, user User, report_id string) (*models.PublicreportReport, error) {
|
||||
func reportFromID(ctx context.Context, report_id string) (*models.PublicreportReport, error) {
|
||||
report, err := models.PublicreportReports.Query(
|
||||
models.SelectWhere.PublicreportReports.PublicID.EQ(report_id),
|
||||
models.SelectWhere.PublicreportReports.OrganizationID.EQ(user.Organization.ID),
|
||||
).One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
func reportUpdateLocation(ctx context.Context, txn bob.Executor, id int32, location types.Location) error {
|
||||
h3cell, _ := location.H3Cell()
|
||||
geom_query, _ := location.GeometryQuery()
|
||||
_, err := psql.Update(
|
||||
um.Table("publicreport.report"),
|
||||
um.SetCol("h3cell").ToArg(h3cell),
|
||||
um.SetCol("location").To(geom_query),
|
||||
um.Where(psql.Quote("id").EQ(psql.Arg(id))),
|
||||
).Exec(ctx, txn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to insert publicreport.report geospatial", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@ import (
|
|||
)
|
||||
|
||||
type Address struct {
|
||||
Country string `db:"country" json:"country"`
|
||||
GID string `db:"gid" json:"gid" schema:"gid"`
|
||||
Locality string `db:"locality" json:"locality"`
|
||||
Number string `db:"number" json:"number"`
|
||||
PostalCode string `db:"postal_code" json:"postal_code"`
|
||||
Raw string `db:"raw" json:"raw" schema:"raw"`
|
||||
Region string `db:"region" json:"region"`
|
||||
Street string `db:"street" json:"street"`
|
||||
Unit string `db:"unit" json:"unit"`
|
||||
Country string `db:"country" json:"country"`
|
||||
GID string `db:"gid" json:"gid" schema:"gid"`
|
||||
Locality string `db:"locality" json:"locality"`
|
||||
Location *Location `db:"location" json:"location" schema:"location"`
|
||||
Number string `db:"number" json:"number"`
|
||||
PostalCode string `db:"postal_code" json:"postal_code"`
|
||||
Raw string `db:"raw" json:"raw" schema:"raw"`
|
||||
Region string `db:"region" json:"region"`
|
||||
Street string `db:"street" json:"street"`
|
||||
Unit string `db:"unit" json:"unit"`
|
||||
}
|
||||
|
||||
func (a Address) String() string {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
type Report struct {
|
||||
Address Address `db:"address" json:"address"`
|
||||
AddressRaw string `db:"address_raw" json:"address_raw"`
|
||||
Created time.Time `db:"created" json:"created"`
|
||||
ID int32 `db:"id" json:"-"`
|
||||
Images []Image `db:"images" json:"images"`
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import (
|
|||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/html"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
//"github.com/Gleipnir-Technology/nidus-sync/html"
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/rs/zerolog/log"
|
||||
//"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Compliance(r *router) *complianceR {
|
||||
|
|
@ -92,20 +92,3 @@ func (res *complianceR) Create(ctx context.Context, r *http.Request, n complianc
|
|||
URI: uri,
|
||||
}, nil
|
||||
}
|
||||
func (res *complianceR) Update(ctx context.Context, r *http.Request, n complianceForm) (*compliance, *nhttp.ErrorWithStatus) {
|
||||
uploads, err := html.ExtractImageUploads(r)
|
||||
log.Info().Int("len", len(uploads)).Msg("extracted compliance uploads")
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("Failed to extract image uploads: %w", err)
|
||||
}
|
||||
address := platform.Address{
|
||||
GID: n.Locator.Address.GID,
|
||||
Raw: n.Locator.Address.Raw,
|
||||
}
|
||||
accuracy := float32(0.0)
|
||||
if n.Location.Accuracy != nil {
|
||||
accuracy = *n.Location.Accuracy
|
||||
}
|
||||
log.Info().Str("address.raw", address.Raw).Str("address.gid", address.GID).Float32("accuracy", accuracy).Msg("making compliance")
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package resource
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/aarondl/opt/omit"
|
||||
//"github.com/aarondl/opt/omitnull"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"net/http"
|
||||
//"github.com/rs/zerolog/log"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
|
@ -46,3 +49,55 @@ func (res *publicreportR) ByID(ctx context.Context, r *http.Request, query Query
|
|||
report.URI = uri
|
||||
return report, nil
|
||||
}
|
||||
|
||||
type publicreportForm struct {
|
||||
Address *types.Address `schema:"address"`
|
||||
ClientID string `schema:"client_id"`
|
||||
DistrictID string `schema:"district"`
|
||||
Location *types.Location `schema:"location"`
|
||||
Locator *Locator `schema:"locator"`
|
||||
Reporter *types.Contact `schema:"reporter"`
|
||||
}
|
||||
|
||||
func (res *publicreportR) Update(ctx context.Context, r *http.Request, prf publicreportForm) (*types.Report, *nhttp.ErrorWithStatus) {
|
||||
/*
|
||||
uploads, err := html.ExtractImageUploads(r)
|
||||
log.Info().Int("len", len(uploads)).Msg("extracted compliance uploads")
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("Failed to extract image uploads: %w", err)
|
||||
}
|
||||
*/
|
||||
vars := mux.Vars(r)
|
||||
public_id := vars["id"]
|
||||
if public_id == "" {
|
||||
return nil, nhttp.NewBadRequest("You must provide an ID")
|
||||
}
|
||||
report_setter := models.PublicreportReportSetter{}
|
||||
if prf.Address != nil {
|
||||
report_setter.AddressGid = omit.From(prf.Address.GID)
|
||||
report_setter.AddressRaw = omit.From(prf.Address.Raw)
|
||||
}
|
||||
if prf.Location != nil {
|
||||
//report_setter.Latitude = omit.From(prf.Location.Latitude)
|
||||
//report_setter.Longitude = omit.From(prf.Location.Longitude)
|
||||
if prf.Location.Accuracy != nil {
|
||||
report_setter.LatlngAccuracyValue = omit.From(*prf.Location.Accuracy)
|
||||
}
|
||||
}
|
||||
if prf.Reporter != nil {
|
||||
if prf.Reporter.Email != nil {
|
||||
report_setter.ReporterEmail = omit.From(*prf.Reporter.Email)
|
||||
}
|
||||
if prf.Reporter.Name != nil {
|
||||
report_setter.ReporterName = omit.From(*prf.Reporter.Name)
|
||||
}
|
||||
if prf.Reporter.Phone != nil {
|
||||
report_setter.ReporterPhone = omit.From(*prf.Reporter.Phone)
|
||||
}
|
||||
}
|
||||
report, err := platform.PublicReportUpdate(ctx, public_id, report_setter, prf.Location)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("update report: %w", err)
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export class Address {
|
|||
public region: string = "",
|
||||
public street: string = "",
|
||||
public unit: string = "",
|
||||
public location?: Location,
|
||||
) {}
|
||||
}
|
||||
export interface Bounds {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue