Add concern page to mailer compliance flow

This commit is contained in:
Eli Ribble 2026-04-22 21:22:03 +00:00
parent b5923137a7
commit a8819c907e
No known key found for this signature in database
11 changed files with 206 additions and 124 deletions

View file

@ -67,8 +67,8 @@ func PublicreportByIDCompliance(ctx context.Context, report_id string) (*types.P
return nil, fmt.Errorf("compliance report request by public id: %w", err)
}
if crr != nil {
result.Evidence = []*types.EvidenceComplianceReportRequest{
&types.EvidenceComplianceReportRequest{
result.Concerns = []*types.ConcernComplianceReportRequest{
&types.ConcernComplianceReportRequest{
ComplianceReportRequestPublicID: crr.PublicID,
},
}

View file

@ -7,15 +7,15 @@ import (
"github.com/rs/zerolog/log"
)
type Evidence interface {
type Concern interface {
PopulateURL(*mux.Router) error
}
type EvidenceComplianceReportRequest struct {
ComplianceReportRequestPublicID string
URL string
type ConcernComplianceReportRequest struct {
ComplianceReportRequestPublicID string `json:"compliance_report_request_public_id"`
URL string `json:"url"`
}
func (e *EvidenceComplianceReportRequest) PopulateURL(r *mux.Router) error {
func (e *ConcernComplianceReportRequest) PopulateURL(r *mux.Router) error {
route_name := "compliance-request.image.pool.ByIDGet"
handler := r.Get(route_name)
if handler == nil {
@ -27,6 +27,6 @@ func (e *EvidenceComplianceReportRequest) PopulateURL(r *mux.Router) error {
}
uri.Scheme = "https"
e.URL = uri.String()
log.Debug().Str("url", e.URL).Msg("populated evidence URL")
log.Debug().Str("url", e.URL).Msg("populated concern URL")
return nil
}

View file

@ -5,20 +5,20 @@ import (
)
type PublicReport struct {
Address Address `db:"address" json:"address"`
Created time.Time `db:"created" json:"created"`
Evidence []*EvidenceComplianceReportRequest `db:"-" json:"evidence"`
ID int32 `db:"id" json:"-"`
Images []Image `db:"images" json:"images"`
Location *Location `db:"location" json:"location"`
Log []LogEntry `db:"-" json:"log"`
DistrictID *int32 `db:"organization_id" json:"-"`
District *string `db:"-" json:"district"`
PublicID string `db:"public_id" json:"public_id"`
Reporter Contact `db:"reporter" json:"reporter"`
Status string `db:"status" json:"status"`
Type string `db:"report_type" json:"type"`
URI string `db:"-" json:"uri"`
Address Address `db:"address" json:"address"`
Concerns []*ConcernComplianceReportRequest `db:"-" json:"concerns"`
Created time.Time `db:"created" json:"created"`
ID int32 `db:"id" json:"-"`
Images []Image `db:"images" json:"images"`
Location *Location `db:"location" json:"location"`
Log []LogEntry `db:"-" json:"log"`
DistrictID *int32 `db:"organization_id" json:"-"`
District *string `db:"-" json:"district"`
PublicID string `db:"public_id" json:"public_id"`
Reporter Contact `db:"reporter" json:"reporter"`
Status string `db:"status" json:"status"`
Type string `db:"report_type" json:"type"`
URI string `db:"-" json:"uri"`
}
type PublicReportCompliance struct {
PublicReport

View file

@ -209,7 +209,7 @@ func (res *complianceR) Update(ctx context.Context, r *http.Request, prf publicr
func (res *complianceR) complianceHydrate(report *types.PublicReportCompliance) (*types.PublicReportCompliance, *nhttp.ErrorWithStatus) {
populateDistrictURI(&report.PublicReport, res.router)
populateReportURI(&report.PublicReport, res.router)
for _, e := range report.Evidence {
for _, e := range report.Concerns {
e.PopulateURL(res.router.router)
}
return report, nil

View file

@ -0,0 +1,60 @@
<style scoped>
.modal.show {
background-color: rgba(0, 0, 0, 0.5);
}
img {
min-width: 512px;
min-height: 512px;
}
</style>
<template>
<div
class="modal fade"
:class="{ 'show d-block': show }"
tabindex="-1"
v-show="show"
@click.self="emit('close')"
>
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Image Viewer</h5>
<button
type="button"
class="btn-close"
@click="emit('close')"
></button>
</div>
<div class="modal-body text-center">
<div v-if="image && show">
<img
:src="image.src"
class="img-fluid rounded"
style="max-height: 60vh"
/>
</div>
</div>
</div>
</div>
</div>
<div
class="modal-backdrop fade show"
v-show="show"
@click="emit('close')"
></div>
</template>
<script setup lang="ts">
interface Emits {
(e: "close"): void;
}
export interface Image {
src: string;
}
interface Props {
image?: Image;
show: boolean;
}
const emit = defineEmits<Emits>();
const props = defineProps<Props>();
</script>

View file

@ -65,8 +65,10 @@ const routes = useRoutes();
function doContinue() {
emit("update:modelValue", props.modelValue);
emit("doAddress");
// re-add when we have the concern data to show
// router.push("./concern");
router.push(routes.ComplianceEvidence(props.publicID));
if (props.modelValue.concerns.length > 0) {
router.push(routes.ComplianceConcern(props.publicID));
} else {
router.push(routes.ComplianceEvidence(props.publicID));
}
}
</script>

View file

@ -1,7 +1,5 @@
<style scoped>
.observation-image {
width: 100%;
height: 200px;
.concern-image {
background-color: #e9ecef;
border: 1px solid #dee2e6;
border-radius: 8px;
@ -15,12 +13,12 @@
overflow: hidden;
}
.observation-image:hover {
.concern-image:hover {
border-color: #0d6efd;
box-shadow: 0 2px 8px rgba(13, 110, 253, 0.2);
}
.observation-image .overlay {
.concern-image .overlay {
position: absolute;
bottom: 0;
left: 0;
@ -53,84 +51,26 @@
</h2>
<p class="text-muted mb-4">
Our inspector documented the following conditions during their visit.
Tap any image to view details.
The following images show areas we are concerned about related to your
property. Tap any image to view details.
</p>
<!-- Observation Images -->
<div class="mb-4">
<label class="form-label fw-semibold mb-3">Inspection Photos</label>
<label class="form-label fw-semibold mb-3">Site Photos</label>
<div class="row g-3 mb-3">
<div class="col-6">
<div
class="observation-image"
data-bs-toggle="modal"
data-bs-target="#imageModal1"
>
<span class="text-center">
<i class="bi bi-camera" style="font-size: 2rem"></i>
<br />Photo 1
</span>
<div class="overlay">Tap to view</div>
<div class="card">
<div class="card-body">
<img
class="concern-image"
@click="openImageViewer(index)"
:key="index"
:src="concern.url"
v-for="(concern, index) in modelValue.concerns"
/>
</div>
</div>
<div class="col-6">
<div
class="observation-image"
data-bs-toggle="modal"
data-bs-target="#imageModal2"
>
<span class="text-center">
<i class="bi bi-camera" style="font-size: 2rem"></i>
<br />Photo 2
</span>
<div class="overlay">Tap to view</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-6">
<div
class="observation-image"
data-bs-toggle="modal"
data-bs-target="#imageModal3"
>
<span class="text-center">
<i class="bi bi-camera" style="font-size: 2rem"></i>
<br />Photo 3
</span>
<div class="overlay">Tap to view</div>
</div>
</div>
<div class="col-6">
<div
class="observation-image"
data-bs-toggle="modal"
data-bs-target="#imageModal4"
>
<span class="text-center">
<i class="bi bi-camera" style="font-size: 2rem"></i>
<br />Photo 4
</span>
<div class="overlay">Tap to view</div>
</div>
</div>
</div>
</div>
<!-- Inspector Comments -->
<div class="mb-4">
<label class="form-label fw-semibold mb-3">Inspector Comments</label>
<div class="inspector-notes">
<div class="mb-2">
<small class="text-muted">
<i class="bi bi-person-badge"></i>
Inspector #1 | 2 days ago
</small>
</div>
<p class="mb-0">some fake comments here</p>
</div>
</div>
@ -145,22 +85,74 @@
<!-- Navigation Buttons -->
<div class="d-flex gap-2 mt-4">
<RouterLink to="./address" class="btn btn-outline-secondary">
<RouterLink
class="btn btn-outline-secondary"
:to="routes.ComplianceAddress(props.publicID)"
>
Back
</RouterLink>
<RouterLink to="evidence" class="btn btn-primary flex-grow-1">
<button class="btn btn-primary flex-grow-1" @click="doContinue">
Continue
</RouterLink>
</button>
</div>
</main>
</div>
<ImageViewerModal
@close="showImageModal = false"
:image="currentImage"
:show="showImageModal"
/>
</template>
<script setup lang="ts">
import type { District } from "@/type/api";
import { computed, onMounted, ref } from "vue";
import HeaderCompliance from "@/rmo/components/HeaderCompliance.vue";
import ImageViewerModal, { Image } from "@/rmo/components/ImageViewerModal.vue";
import ProgressBarCompliance from "@/rmo/components/ProgressBarCompliance.vue";
import { router } from "@/rmo/route/config";
import { useRoutes } from "@/rmo/route/use";
import type { District, PublicReportCompliance } from "@/type/api";
interface Emits {
(e: "doConcern"): void;
(e: "update:modelValue", value: PublicReportCompliance): void;
}
interface Props {
district: District;
modelValue: PublicReportCompliance;
publicID: string;
}
const currentImageIndex = ref<number>(0);
const emit = defineEmits<Emits>();
const isLoading = ref<boolean>(true);
const props = defineProps<Props>();
const routes = useRoutes();
const showImageModal = ref(false);
const currentImage = computed((): Image | undefined => {
const i = props.modelValue.concerns[currentImageIndex.value] ?? null;
if (!i) {
return undefined;
}
return {
src: i.url,
};
});
function doContinue() {
emit("update:modelValue", props.modelValue);
emit("doConcern");
router.push(routes.ComplianceEvidence(props.publicID));
}
async function doMounted() {
if (props.modelValue.concerns.length == 0) {
router.push(routes.ComplianceEvidence(props.publicID));
}
}
function openImageViewer(index: number) {
currentImageIndex.value = index;
showImageModal.value = true;
}
onMounted(() => {
doMounted();
});
</script>

View file

@ -147,10 +147,7 @@
<!-- Navigation Buttons -->
<div class="d-flex gap-2 mt-4">
<RouterLink
class="btn btn-outline-secondary"
:to="routes.ComplianceAddress(publicID)"
>
<RouterLink class="btn btn-outline-secondary" :to="toBack()">
Back
</RouterLink>
<button class="btn btn-primary flex-grow-1" @click="doContinue">
@ -188,4 +185,11 @@ function doContinue() {
emit("doEvidence", images.value);
router.push(routes.CompliancePermission(props.publicID));
}
function toBack() {
if (props.modelValue.concerns.length > 0) {
return routes.ComplianceConcern(props.publicID);
} else {
return routes.ComplianceAddress(props.publicID);
}
}
</script>

View file

@ -172,18 +172,7 @@ async function updateReport(updates: ComplianceUpdate) {
console.log("Refusing to update report without URI");
return;
}
const resp = await fetch(report.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;
}
storePublicReport.update(report.value.uri, updates);
}
async function updateLocation() {
if (!report.value) return;

View file

@ -6,6 +6,7 @@ import {
PublicReport,
type PublicReportComplianceCreateRequest,
type PublicReportDTO,
type PublicReportUpdate,
} from "@/type/api";
export const useStorePublicReport = defineStore("publicreport", () => {
@ -64,6 +65,13 @@ export const useStorePublicReport = defineStore("publicreport", () => {
throw err;
}
}
async function update(
uri: string,
updates: PublicReportUpdate,
): Promise<PublicReport> {
const resp = (await apiClient.JSONPut(uri, updates)) as PublicReportDTO;
return PublicReport.fromJSON(resp);
}
return {
// Actions
add,
@ -72,5 +80,6 @@ export const useStorePublicReport = defineStore("publicreport", () => {
createCompliance,
fetchByID,
fetchByURI,
update,
};
});

View file

@ -158,22 +158,35 @@ export interface ComplianceUpdate {
//uri: string;
wants_scheduled?: boolean;
}
export interface Concern {
type: string;
url: string;
}
export interface PublicReportDTO {
address: Address;
//compliance?: PublicReportCompliance;
created: string;
district: string;
images: Image[];
location: Location;
log: LogEntryDTO[];
//nuisance?: Nuisance;
public_id: string;
reporter: Contact;
status: string;
type: string;
//water?: Water;
uri: string;
}
export interface PublicReportUpdate {
address?: Address;
created?: string;
district?: string;
images?: Image[];
location?: Location;
public_id?: string;
reporter?: Contact;
status?: string;
type?: string;
uri?: string;
}
export interface PublicReportComplianceCreateRequest {
client_id: string;
district?: string;
@ -236,6 +249,7 @@ export interface PublicReportComplianceDTO extends PublicReportDTO {
access_instructions: string;
availability_notes: string;
comments: string;
concerns: Concern[];
gate_code: string;
has_dog: boolean;
permission_type: PermissionType;
@ -245,15 +259,26 @@ export interface PublicReportComplianceOptions extends PublicReportOptions {
access_instructions: string;
availability_notes: string;
comments: string;
concerns: Concern[];
gate_code: string;
has_dog: boolean;
permission_type: PermissionType;
wants_scheduled: boolean;
}
export interface PublicReportComplianceUpdate extends PublicReportUpdate {
access_instructions?: string;
availability_notes?: string;
comments?: string;
gate_code?: string;
has_dog?: boolean;
permission_type?: PermissionType;
wants_scheduled?: boolean;
}
export class PublicReportCompliance extends PublicReport {
access_instructions: string;
availability_notes: string;
comments: string;
concerns: Concern[];
gate_code: string;
has_dog: boolean;
permission_type: PermissionType;
@ -263,6 +288,7 @@ export class PublicReportCompliance extends PublicReport {
this.access_instructions = options?.access_instructions ?? "";
this.availability_notes = options?.availability_notes ?? "";
this.comments = options?.comments ?? "";
this.concerns = options?.concerns ?? [];
this.gate_code = options?.gate_code ?? "";
this.has_dog = options?.has_dog ?? false;
this.permission_type = toPermissionType(