Split out ComplianceDistrict view for creating new compliance reports
The idea here is that we'll make compliance reports two different ways, The first is if the user navigates to /district/:slug/compliance, the second if they open a QR code from a mailer. In both cases we create the report then feed them into a flow for updating the data on that report.
This commit is contained in:
parent
8eae73eefb
commit
f927b0a911
7 changed files with 154 additions and 241 deletions
|
|
@ -265,11 +265,13 @@ func publicReportCreate(ctx context.Context, setter_report models.PublicreportRe
|
|||
}
|
||||
defer txn.Rollback(ctx)
|
||||
|
||||
public_id, err := GenerateReportID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create public ID: %w", err)
|
||||
if setter_report.PublicID.IsUnset() {
|
||||
public_id, err := GenerateReportID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create public ID: %w", err)
|
||||
}
|
||||
setter_report.PublicID = omit.From(public_id)
|
||||
}
|
||||
setter_report.PublicID = omit.From(public_id)
|
||||
|
||||
var addr *models.Address
|
||||
if address != nil && address.GID != "" {
|
||||
|
|
@ -305,7 +307,7 @@ func publicReportCreate(ctx context.Context, setter_report models.PublicreportRe
|
|||
publicReportUpdateLocation(ctx, txn, result.ID, l)
|
||||
}
|
||||
}
|
||||
log.Info().Str("public_id", public_id).Int32("id", result.ID).Msg("Created base report")
|
||||
log.Info().Str("public_id", setter_report.PublicID.GetOr("")).Int32("id", result.ID).Msg("Created base report")
|
||||
|
||||
if len(saved_images) > 0 {
|
||||
setters := make([]*models.PublicreportReportImageSetter, 0)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ type publicreportComplianceForm struct {
|
|||
GateCode omit.Val[string] `schema:"gate_code" json:"gate_code"`
|
||||
HasDog omitnull.Val[bool] `schema:"has_dog" json:"has_dog"`
|
||||
Location omit.Val[types.Location] `schema:"location" json:"location"`
|
||||
MailerID omit.Val[string] `schema:"mailer_id" json:"mailer_id"`
|
||||
PermissionType omit.Val[enums.Permissionaccesstype] `schema:"permission_type" json:"permission_type"`
|
||||
Reporter omit.Val[types.Contact] `schema:"reporter" json:"reporter"`
|
||||
ReportPhoneCanSMS omitnull.Val[bool] `schema:"report_phone_can_text" json:"report_phone_can_text"`
|
||||
|
|
@ -89,7 +90,7 @@ func (res *complianceR) Create(ctx context.Context, r *http.Request, n publicrep
|
|||
Location: omitnull.FromPtr[string](nil),
|
||||
MapZoom: omit.From(float32(0.0)),
|
||||
//OrganizationID: omit.From[int32](int32(*district_id)),
|
||||
//PublicID: omit.From(public_id),
|
||||
PublicID: n.MailerID,
|
||||
ReporterEmail: omit.From(""),
|
||||
ReporterName: omit.From(""),
|
||||
ReporterPhone: omit.From(""),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import ComplianceAddress from "@/rmo/content/compliance/Address.vue";
|
|||
import ComplianceComplete from "@/rmo/content/compliance/Complete.vue";
|
||||
import ComplianceConcern from "@/rmo/content/compliance/Concern.vue";
|
||||
import ComplianceContact from "@/rmo/content/compliance/Contact.vue";
|
||||
import ComplianceDistrict from "@/rmo/view/ComplianceDistrict.vue";
|
||||
import ComplianceEvidence from "@/rmo/content/compliance/Evidence.vue";
|
||||
import ComplianceIntro from "@/rmo/content/compliance/Intro.vue";
|
||||
import ComplianceMailer from "@/rmo/view/ComplianceMailer.vue";
|
||||
|
|
@ -87,10 +88,16 @@ const routes: RouteRecordRaw[] = [
|
|||
},
|
||||
],
|
||||
component: Compliance,
|
||||
path: "/district/:slug/compliance",
|
||||
path: "/compliance/:public_id",
|
||||
name: "Compliance",
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
component: ComplianceDistrict,
|
||||
path: "/district/:slug/compliance",
|
||||
name: "ComplianceDistrict",
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/district/:slug/nuisance",
|
||||
name: "NuisanceDistrict",
|
||||
|
|
@ -110,53 +117,6 @@ const routes: RouteRecordRaw[] = [
|
|||
props: true,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
component: ComplianceIntro,
|
||||
name: "ComplianceIntro",
|
||||
path: "",
|
||||
},
|
||||
{
|
||||
component: ComplianceAddress,
|
||||
name: "ComplianceAddress",
|
||||
path: "address",
|
||||
},
|
||||
{
|
||||
component: ComplianceComplete,
|
||||
name: "ComplianceComplete",
|
||||
path: "complete",
|
||||
},
|
||||
{
|
||||
component: ComplianceConcern,
|
||||
name: "ComplianceConcern",
|
||||
path: "concern",
|
||||
},
|
||||
{
|
||||
component: ComplianceContact,
|
||||
name: "ComplianceContact",
|
||||
path: "contact",
|
||||
},
|
||||
{
|
||||
component: ComplianceEvidence,
|
||||
name: "ComplianceEvidence",
|
||||
path: "evidence",
|
||||
},
|
||||
{
|
||||
component: CompliancePermission,
|
||||
name: "CompliancePermission",
|
||||
path: "permission",
|
||||
},
|
||||
{
|
||||
component: ComplianceProcess,
|
||||
name: "ComplianceProcess",
|
||||
path: "process",
|
||||
},
|
||||
{
|
||||
component: ComplianceSubmit,
|
||||
name: "ComplianceSubmit",
|
||||
path: "submit",
|
||||
},
|
||||
],
|
||||
path: "/mailer/:public_id",
|
||||
name: "ComplianceMailer",
|
||||
component: ComplianceMailer,
|
||||
|
|
|
|||
72
ts/rmo/view/ComplianceDistrict.vue
Normal file
72
ts/rmo/view/ComplianceDistrict.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="row min-vh-100 align-items-center justify-content-center">
|
||||
<div class="col-auto text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3 text-muted">Loading report details...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computedAsync } from "@vueuse/core";
|
||||
|
||||
import type { Image } from "@/components/ImageUpload.vue";
|
||||
import { useStoreDistrict } from "@/rmo/store/district";
|
||||
import { useStoreLocal } from "@/store/local";
|
||||
import { useStorePublicReport } from "@/store/publicreport";
|
||||
import Intro from "@/rmo/content/compliance/Intro.vue";
|
||||
import LoadingOverlay from "@/components/LoadingOverlay.vue";
|
||||
import {
|
||||
type ComplianceUpdate,
|
||||
type District,
|
||||
PublicReport,
|
||||
PublicReportCompliance,
|
||||
PublicReportComplianceOptions,
|
||||
} from "@/type/api";
|
||||
import { Contact, Address, PermissionType } from "@/type/api";
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const districtStore = useStoreDistrict();
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const router = useRouter();
|
||||
const storeLocal = useStoreLocal();
|
||||
const storePublicReport = useStorePublicReport();
|
||||
async function doMounted() {
|
||||
const client_id = storeLocal.getClientID();
|
||||
const report_uri = storeLocal.getExistingComplianceReportURI();
|
||||
if (report_uri) {
|
||||
const report = await storePublicReport.byURI(report_uri);
|
||||
if (report && report.public_id) {
|
||||
router.replace(`/compliance/${report.public_id}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const districts = await districtStore.list();
|
||||
const district = districts.find(
|
||||
(district: District) => district.slug == props.slug,
|
||||
);
|
||||
if (!district) {
|
||||
console.error("failed to find matching district", props.slug, districts);
|
||||
return;
|
||||
}
|
||||
const report = await storePublicReport.create({
|
||||
client_id: client_id,
|
||||
district: district.uri,
|
||||
});
|
||||
storeLocal.setExistingComplianceReportURI(report.uri);
|
||||
router.replace(`/compliance/${report.public_id}`);
|
||||
}
|
||||
onMounted(() => {
|
||||
doMounted();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,54 +1,19 @@
|
|||
<style scoped>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
body > .container-fluid {
|
||||
flex: 1;
|
||||
}
|
||||
.progress-bar {
|
||||
background-color: #0d6efd;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.reference-number {
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<LoadingOverlay
|
||||
:is-loading="isLoading"
|
||||
loading-text="Loading previous data"
|
||||
>
|
||||
<template v-if="!isLoading">
|
||||
<component
|
||||
:is="Component"
|
||||
:district="district"
|
||||
@doAddress="doAddress"
|
||||
@doContact="doContact"
|
||||
@doEvidence="doEvidence"
|
||||
@doPermission="doPermission"
|
||||
@doSubmit="doSubmit"
|
||||
v-model="report"
|
||||
/>
|
||||
</template>
|
||||
</LoadingOverlay>
|
||||
</router-view>
|
||||
<!-- Reference Number -->
|
||||
<div class="reference-number" v-if="report && report.public_id">
|
||||
<small>
|
||||
Reference number: <strong>{{ report.public_id }}</strong>
|
||||
</small>
|
||||
<div class="container">
|
||||
<div class="row min-vh-100 align-items-center justify-content-center">
|
||||
<div class="col-auto text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3 text-muted">Loading report details...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computedAsync } from "@vueuse/core";
|
||||
|
||||
import type { Image } from "@/components/ImageUpload.vue";
|
||||
|
|
@ -72,154 +37,39 @@ interface Props {
|
|||
|
||||
const districtStore = useStoreDistrict();
|
||||
|
||||
const isLoading = ref<boolean>(true);
|
||||
const isUploading = ref<boolean>(false);
|
||||
const props = defineProps<Props>();
|
||||
const report = ref<PublicReportCompliance>(new PublicReportCompliance());
|
||||
const district = ref<District | undefined>(undefined);
|
||||
const router = useRouter();
|
||||
const storeLocal = useStoreLocal();
|
||||
const storeLocation = useStoreLocation();
|
||||
async function beginReport(client_id: string) {
|
||||
const report_uri = "/api/publicreport/compliance/" + props.public_id;
|
||||
const [districts, r] = await Promise.all([
|
||||
districtStore.list(),
|
||||
fetchExistingReport(report_uri),
|
||||
]);
|
||||
Object.assign(report.value, r);
|
||||
const d = districts.find((district: District) => district.uri == r.district);
|
||||
if (!d) {
|
||||
console.error("Failed to find district with uri", districts, r.district);
|
||||
return;
|
||||
}
|
||||
district.value = d;
|
||||
isLoading.value = false;
|
||||
await updateLocation();
|
||||
}
|
||||
function doAddress() {
|
||||
if (!report.value) {
|
||||
console.log("can't do address, null report");
|
||||
return;
|
||||
}
|
||||
console.log("address done", report.value.address);
|
||||
updateReport({
|
||||
address: report.value.address,
|
||||
});
|
||||
}
|
||||
function doEvidence(images: Image[]) {
|
||||
if (!report.value) {
|
||||
console.log("can't do evidence, null report");
|
||||
return;
|
||||
}
|
||||
uploadImages(images);
|
||||
if (report.value.comments) {
|
||||
updateReport({
|
||||
comments: report.value.comments,
|
||||
});
|
||||
}
|
||||
}
|
||||
function doContact() {
|
||||
if (!report.value) {
|
||||
console.log("can't do contact, null report");
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
"contact",
|
||||
JSON.stringify(report.value.reporter),
|
||||
report.value.reporter,
|
||||
);
|
||||
updateReport({
|
||||
reporter: report.value.reporter,
|
||||
});
|
||||
}
|
||||
function doPermission() {
|
||||
if (!report.value) {
|
||||
console.log("can't do permission, null report");
|
||||
return;
|
||||
}
|
||||
console.log("report.value.has_dog", report.value.has_dog);
|
||||
updateReport({
|
||||
access_instructions: report.value.access_instructions,
|
||||
availability_notes: report.value.availability_notes,
|
||||
gate_code: report.value.gate_code,
|
||||
has_dog: report.value.has_dog,
|
||||
permission_type: report.value.permission_type,
|
||||
wants_scheduled: report.value.wants_scheduled,
|
||||
});
|
||||
}
|
||||
function doSubmit() {
|
||||
console.log("submit", report.value);
|
||||
storeLocal.delExistingComplianceReportURI();
|
||||
}
|
||||
async function fetchExistingReport(
|
||||
report_uri: string,
|
||||
async function createReport(
|
||||
client_id: string,
|
||||
): Promise<PublicReportCompliance> {
|
||||
const resp = await fetch(report_uri);
|
||||
if (!resp.ok) {
|
||||
const content = await resp.text();
|
||||
throw new Error(
|
||||
`Failed to fetch existing report ${report_uri}: ${resp.status} ${content}`,
|
||||
);
|
||||
}
|
||||
const body = (await resp.json()) as PublicReportComplianceOptions;
|
||||
console.log("fetched existing report", report.value);
|
||||
return new PublicReportCompliance(body);
|
||||
}
|
||||
async function updateReport(updates: ComplianceUpdate) {
|
||||
if (!report.value.uri) {
|
||||
console.log("Refusing to update report without URI");
|
||||
return;
|
||||
}
|
||||
const resp = await fetch(report.value.uri, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(updates),
|
||||
let content = {
|
||||
client_id: client_id,
|
||||
mailer_id: props.public_id,
|
||||
};
|
||||
const resp = await fetch("/api/rmo/compliance", {
|
||||
body: JSON.stringify(content),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const content = await resp.text();
|
||||
console.error("Failed to update compliance", resp.status, content);
|
||||
return;
|
||||
}
|
||||
}
|
||||
async function updateLocation() {
|
||||
const loc = await storeLocation.get();
|
||||
report.value.location = loc.coords;
|
||||
updateReport({
|
||||
location: report.value.location,
|
||||
});
|
||||
}
|
||||
async function uploadImages(images: Image[]) {
|
||||
if (images.length == 0) return;
|
||||
isUploading.value = true;
|
||||
const formData = new FormData();
|
||||
images.map(async (image, index) => {
|
||||
formData.append(`image[${index}]`, image.file, image.name);
|
||||
});
|
||||
const url = `${report.value.uri}/image`;
|
||||
const response = await fetch(url, {
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const content = await response.text();
|
||||
console.error(
|
||||
"Failed to POST images",
|
||||
url,
|
||||
response.status,
|
||||
response.statusText,
|
||||
content,
|
||||
);
|
||||
isUploading.value = false;
|
||||
return;
|
||||
const body = (await resp.json()) as PublicReportComplianceOptions;
|
||||
return new PublicReportCompliance(body);
|
||||
}
|
||||
async function doMounted() {
|
||||
const client_id = storeLocal.getClientID();
|
||||
const report_uri = storeLocal.getExistingComplianceReportURI();
|
||||
if (report_uri && report_uri.endsWith(props.public_id)) {
|
||||
console.log("Loading previous report", report_uri);
|
||||
} else {
|
||||
const report = await createReport(client_id);
|
||||
storeLocal.setExistingComplianceReportURI(report.uri);
|
||||
console.log("Created new compliance report", report);
|
||||
}
|
||||
isUploading.value = false;
|
||||
// after everything is done update the report so that we see the correct number of images
|
||||
// on the report summary
|
||||
await fetchExistingReport(report.value.uri);
|
||||
router.replace(`/compliance/${props.public_id}`);
|
||||
}
|
||||
onMounted(() => {
|
||||
const client_id = storeLocal.getClientID();
|
||||
beginReport(client_id);
|
||||
doMounted();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { PublicReport, type PublicReportDTO } from "@/type/api";
|
||||
|
||||
import { apiClient } from "@/client";
|
||||
import {
|
||||
PublicReport,
|
||||
type PublicReportCreateRequest,
|
||||
type PublicReportDTO,
|
||||
} from "@/type/api";
|
||||
|
||||
export const useStorePublicReport = defineStore("publicreport", () => {
|
||||
// State
|
||||
const _byID = ref<Map<string, PublicReport>>(new Map());
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
//const ongoingFetch = ref<Promise<PublicReport[]> | null>(null);
|
||||
|
||||
|
|
@ -15,7 +20,6 @@ export const useStorePublicReport = defineStore("publicreport", () => {
|
|||
// Actions
|
||||
async function byID(id: string): Promise<PublicReport | undefined> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const url = `/api/publicreport/${id}`;
|
||||
const response = await fetch(url);
|
||||
|
|
@ -32,10 +36,29 @@ export const useStorePublicReport = defineStore("publicreport", () => {
|
|||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function byURI(uri: string): Promise<PublicReport | undefined> {
|
||||
const id = uri.split("/").pop() || "";
|
||||
if (!id) {
|
||||
throw new Error(`${uri} is not a recognized public report URI`);
|
||||
}
|
||||
return byID(id);
|
||||
}
|
||||
async function create(
|
||||
data: PublicReportCreateRequest,
|
||||
): Promise<PublicReport> {
|
||||
const resp = (await apiClient.JSONPost(
|
||||
"/api/rmo/compliance",
|
||||
data,
|
||||
)) as PublicReportDTO;
|
||||
const result = PublicReport.fromJSON(resp);
|
||||
_byID.value.set(result.public_id, result);
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
// Actions
|
||||
add,
|
||||
byID,
|
||||
byURI,
|
||||
create,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -174,6 +174,11 @@ export interface PublicReportDTO {
|
|||
//water?: Water;
|
||||
uri: string;
|
||||
}
|
||||
export interface PublicReportCreateRequest {
|
||||
client_id: string;
|
||||
district?: string;
|
||||
mailer_id?: string;
|
||||
}
|
||||
export interface PublicReportOptions {
|
||||
address: Address;
|
||||
created: Date;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue