Add concern page to mailer compliance flow
This commit is contained in:
parent
b5923137a7
commit
a8819c907e
11 changed files with 206 additions and 124 deletions
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
60
ts/rmo/components/ImageViewerModal.vue
Normal file
60
ts/rmo/components/ImageViewerModal.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue