Use class heirarchy for different report types.

This commit is contained in:
Eli Ribble 2026-04-10 23:57:47 +00:00
parent 4735734404
commit 60eb6b9bbf
No known key found for this signature in database
13 changed files with 539 additions and 380 deletions

View file

@ -91,13 +91,7 @@ interface Props {
const emit = defineEmits<Emits>();
const props = defineProps<Props>();
const nuisance = computed(() => {
return props.selectedCommunication?.public_report?.nuisance || null;
});
const session = useSessionStore();
const water = computed(() => {
return props.selectedCommunication?.public_report?.water || null;
});
function openPhotoViewer(index: number) {
emit("viewImage", index);
}

View file

@ -0,0 +1,82 @@
<template>
<div class="card-header bg-danger bg-opacity-10">
<i class="bi bi-exclamation-triangle"></i> Nuisance Details
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label text-muted small mb-0">
<i class="bi bi-clock"></i> Time of Day Encountered
</label>
<ul>
<li v-if="report.time_of_day_early">Early</li>
<li v-if="report.time_of_day_day">Daytime</li>
<li v-if="report.time_of_day_evening">Evening</li>
<li v-if="report.time_of_day_night">Night</li>
</ul>
</div>
<div class="col-md-6">
<label class="form-label text-muted small mb-0">
<i class="bi bi-house"></i> Property Area
</label>
<div>
<ul>
<li v-if="report.is_location_backyard">Backyard</li>
<li v-if="report.is_location_frontyard">Frontyard</li>
<li v-if="report.is_location_garden">Garden</li>
<li v-if="report.is_location_other">Other</li>
<li v-if="report.is_location_pool">Pool</li>
</ul>
</div>
</div>
<div
v-if="
report.source_container ||
report.source_gutter ||
report.source_stagnant
"
class="col-md-6"
>
<label class="form-label text-muted small mb-0">
<i class="bi bi-droplet"></i> Sources
</label>
<ul>
<li v-if="report.source_container">Container</li>
<li v-if="report.source_gutter">Gutter</li>
<li v-if="report.source_stagnant">Sprinklers & Gutters</li>
</ul>
</div>
<div v-if="report.source_description" class="col-12">
<label class="form-label text-muted small mb-0">
<i class="bi bi-chat-text"></i> Source Description
</label>
<div class="p-2 bg-light rounded">
{{ report.source_description || "none" }}
</div>
</div>
<div class="col-12">
<label class="form-label text-mudet small mb-0">
<i class="bi bi-clock"></i> Duration
</label>
<div class="p-2 bg-light rounded">
{{ report.duration }}
</div>
</div>
<div class="col-12">
<label class="form-label text-muted small mb-0">
<i class="bi bi-chat-text"></i> Additional Notes
</label>
<div class="p-2 bg-light rounded">
{{ report.additional_info || "No additional notes" }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { PublicReportNuisance } from "@/type/api";
interface Props {
report: PublicReportNuisance;
}
const props = defineProps<Props>();
</script>

View file

@ -0,0 +1,110 @@
<template>
<div class="card-header bg-info bg-opacity-10">
<i class="bi bi-droplet"></i> Standing Water Details
</div>
<div class="card-body">
<div
v-if="
report.access_gate ||
report.access_fence ||
report.access_locked ||
report.access_dog ||
report.access_other
"
class="col-md-6"
>
<label class="form-label text-muted small mb-0">
<i class="bi bi-droplet"></i> Access
</label>
<div>
<ul>
<li v-if="report.access_gate">Gate</li>
<li v-if="report.access_fence">Fence</li>
<li v-if="report.access_locked">Locked</li>
<li v-if="report.access_dog">Dog</li>
<li v-if="report.access_other">Other access obstacle</li>
</ul>
</div>
</div>
<div v-if="report.access_comments" class="col-12">
<label class="form-label text-muted small mb-0">
<i class="bi bi-chat-text"></i> Access Comments
</label>
<div class="p-2 bg-light rounded">
{{ report.access_comments }}
</div>
</div>
<label class="form-label text-muted small mb-0">
<i class="bi bi-eye"></i> Mosquito Life Stages Observed
</label>
<div class="mt-2">
<span
class="badge me-2"
:class="report.has_larvae ? 'badge-larvae' : 'bg-light text-muted'"
>
<i
class="bi"
:class="report.has_larvae ? 'bi-check-circle' : 'bi-circle'"
></i>
Larvae
</span>
<span
class="badge me-2"
:class="report.has_pupae ? 'badge-pupae' : 'bg-light text-muted'"
>
<i
class="bi"
:class="report.has_pupae ? 'bi-check-circle' : 'bi-circle'"
></i>
Pupae
</span>
<span
class="badge"
:class="report.has_adult ? 'badge-adult' : 'bg-light text-muted'"
>
<i
class="bi"
:class="report.has_adult ? 'bi-check-circle' : 'bi-circle'"
></i>
Adult Mosquitoes
</span>
</div>
<div v-if="report.comments" class="col-12">
<label class="form-label text-muted small mb-0">
<i class="bi bi-chat-text"></i> Comments
</label>
<div class="p-2 bg-light rounded">
{{ report.comments }}
</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted small mb-0">
<i class="bi bi-person"></i> Owner Name
</label>
<div class="fw-medium">
{{ report.owner.name || "not given" }}
</div>
</div>
<div class="col-md-6">
<label
v-if="report.owner.has_email"
class="form-label text-muted small mb-0"
>
<i class="bi bi-envelope"></i>
</label>
<label
v-if="report.owner.has_phone"
class="form-label text-muted small mb-0"
>
<i class="bi bi-phone"></i>
</label>
</div>
</div>
</template>
<script setup lang="ts">
import { PublicReportWater } from "@/type/api";
interface Props {
report: PublicReportWater;
}
const props = defineProps<Props>();
</script>

View file

@ -67,13 +67,13 @@
</label>
</div>
</div>
<div v-if="report.water != null" class="row g-3">
<div v-if="report instanceof PublicReportWater" class="row g-3">
<div class="col-12">
<ul>
<li v-if="report.water?.is_reporter_owner">
<li v-if="report.is_reporter_owner">
Reporter is the owner of the property
</li>
<li v-if="report.water?.is_reporter_confidential">
<li v-if="report.is_reporter_confidential">
Reporter has asked to be kept confidential
</li>
</ul>
@ -82,196 +82,12 @@
</div>
</div>
<!-- Nuisance-specific Fields -->
<div v-if="report.nuisance" class="card mb-3">
<div class="card-header bg-danger bg-opacity-10">
<i class="bi bi-exclamation-triangle"></i> Nuisance Details
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label text-muted small mb-0">
<i class="bi bi-clock"></i> Time of Day Encountered
</label>
<ul>
<li v-if="report.nuisance?.time_of_day_early">Early</li>
<li v-if="report.nuisance?.time_of_day_day">Daytime</li>
<li v-if="report.nuisance?.time_of_day_evening">Evening</li>
<li v-if="report.nuisance?.time_of_day_night">Night</li>
</ul>
</div>
<div class="col-md-6">
<label class="form-label text-muted small mb-0">
<i class="bi bi-house"></i> Property Area
</label>
<div>
<ul>
<li v-if="report.nuisance?.is_location_backyard">Backyard</li>
<li v-if="report.nuisance?.is_location_frontyard">Frontyard</li>
<li v-if="report.nuisance?.is_location_garden">Garden</li>
<li v-if="report.nuisance?.is_location_other">Other</li>
<li v-if="report.nuisance?.is_location_pool">Pool</li>
</ul>
</div>
</div>
<div
v-if="
report.nuisance?.source_container ||
report.nuisance?.source_gutter ||
report.nuisance?.source_stagnant
"
class="col-md-6"
>
<label class="form-label text-muted small mb-0">
<i class="bi bi-droplet"></i> Sources
</label>
<ul>
<li v-if="report.nuisance?.source_container">Container</li>
<li v-if="report.nuisance?.source_gutter">Gutter</li>
<li v-if="report.nuisance?.source_stagnant">
Sprinklers & Gutters
</li>
</ul>
</div>
<div v-if="report.nuisance?.source_description" class="col-12">
<label class="form-label text-muted small mb-0">
<i class="bi bi-chat-text"></i> Source Description
</label>
<div class="p-2 bg-light rounded">
{{ report.nuisance?.source_description || "none" }}
</div>
</div>
<div class="col-12">
<label class="form-label text-mudet small mb-0">
<i class="bi bi-clock"></i> Duration
</label>
<div class="p-2 bg-light rounded">
{{ report.nuisance?.duration }}
</div>
</div>
<div class="col-12">
<label class="form-label text-muted small mb-0">
<i class="bi bi-chat-text"></i> Additional Notes
</label>
<div class="p-2 bg-light rounded">
{{ report.nuisance?.additional_info || "No additional notes" }}
</div>
</div>
</div>
</div>
<div v-if="report instanceof PublicReportNuisance">
<PublicReportCardNuisance :report="report" />
</div>
<!-- Standing Water-specific Fields -->
<div v-if="report.water" class="card mb-3">
<div class="card-header bg-info bg-opacity-10">
<i class="bi bi-droplet"></i> Standing Water Details
</div>
<div class="card-body">
<div
v-if="
report.water?.access_gate ||
report.water?.access_fence ||
report.water?.access_locked ||
report.water?.access_dog ||
report.water?.access_other
"
class="col-md-6"
>
<label class="form-label text-muted small mb-0">
<i class="bi bi-droplet"></i> Access
</label>
<div>
<ul>
<li v-if="report.water?.access_gate">Gate</li>
<li v-if="report.water?.access_fence">Fence</li>
<li v-if="report.water?.access_locked">Locked</li>
<li v-if="report.water?.access_dog">Dog</li>
<li v-if="report.water?.access_other">Other access obstacle</li>
</ul>
</div>
</div>
<div v-if="report.water?.access_comments" class="col-12">
<label class="form-label text-muted small mb-0">
<i class="bi bi-chat-text"></i> Access Comments
</label>
<div class="p-2 bg-light rounded">
{{ report.water?.access_comments }}
</div>
</div>
<label class="form-label text-muted small mb-0">
<i class="bi bi-eye"></i> Mosquito Life Stages Observed
</label>
<div class="mt-2">
<span
class="badge me-2"
:class="
report.water?.has_larvae ? 'badge-larvae' : 'bg-light text-muted'
"
>
<i
class="bi"
:class="
report.water?.has_larvae ? 'bi-check-circle' : 'bi-circle'
"
></i>
Larvae
</span>
<span
class="badge me-2"
:class="
report.water?.has_pupae ? 'badge-pupae' : 'bg-light text-muted'
"
>
<i
class="bi"
:class="report.water?.has_pupae ? 'bi-check-circle' : 'bi-circle'"
></i>
Pupae
</span>
<span
class="badge"
:class="
report.water?.has_adult ? 'badge-adult' : 'bg-light text-muted'
"
>
<i
class="bi"
:class="report.water?.has_adult ? 'bi-check-circle' : 'bi-circle'"
></i>
Adult Mosquitoes
</span>
</div>
<div v-if="report.water?.comments" class="col-12">
<label class="form-label text-muted small mb-0">
<i class="bi bi-chat-text"></i> Comments
</label>
<div class="p-2 bg-light rounded">
{{ report.water?.comments }}
</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted small mb-0">
<i class="bi bi-person"></i> Owner Name
</label>
<div class="fw-medium">
{{ report.water?.owner.name || "not given" }}
</div>
</div>
<div class="col-md-6">
<label
v-if="report.water?.owner.has_email"
class="form-label text-muted small mb-0"
>
<i class="bi bi-envelope"></i>
</label>
<label
v-if="report.water?.owner.has_phone"
class="form-label text-muted small mb-0"
>
<i class="bi bi-phone"></i>
</label>
</div>
</div>
<div v-if="report instanceof PublicReportWater">
<PublicReportCardWater :report="report" />
</div>
<!-- Images Section -->
@ -313,10 +129,15 @@
<script setup lang="ts">
import { computed } from "vue";
import MapMultipoint from "@/components/MapMultipoint.vue";
import PublicreportCard from "@/components/PublicreportCard.vue";
import TimeRelative from "@/components/TimeRelative.vue";
import PublicReportCardNuisance from "@/components/PublicReportCardNuisance.vue";
import PublicReportCardWater from "@/components/PublicReportCardWater.vue";
import { formatAddress } from "@/format";
import { PublicReport } from "@/type/api";
import {
PublicReport,
PublicReportNuisance,
PublicReportWater,
} from "@/type/api";
interface Emits {
(e: "viewImage", index: number): void;

View file

@ -50,10 +50,10 @@
/>
</template>
<script setup lang="ts">
import type { Nuisance } from "@/type/api";
import type { PublicReportNuisance } from "@/type/api";
import ReportDetailEntry from "@/rmo/components/ReportDetailEntry.vue";
interface Props {
nuisance: Nuisance;
nuisance: PublicReportNuisance;
}
const props = defineProps<Props>();
</script>

View file

@ -30,20 +30,19 @@
import { computed, ref } from "vue";
import { router } from "@/rmo/router";
import type { District } from "@/type/api";
import type { District, PublicReport } from "@/type/api";
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 } from "@/type/map";
interface Emits {
(e: "doAddress"): void;
(e: "update:modelValue", value: Compliance): void;
(e: "update:modelValue", value: PublicReport): void;
}
interface Props {
district: District;
modelValue: Compliance;
modelValue: PublicReport;
}
const emit = defineEmits<Emits>();
const error = ref<string>("");

View file

@ -45,7 +45,7 @@
id="contact-name"
name="name"
placeholder="Enter your name"
v-model="modelValue.contact.name"
v-model="modelValue.reporter.name"
/>
</div>
@ -61,7 +61,7 @@
id="contact-phone"
name="phone"
placeholder="(555) 123-4567"
v-model="modelValue.contact.phone"
v-model="modelValue.reporter.phone"
/>
</div>
@ -72,7 +72,7 @@
class="form-check-input"
type="checkbox"
id="can-text"
v-model="modelValue.contact.can_text"
v-model="modelValue.reporter.can_text"
/>
<label class="form-check-label" for="can-text">
You may send text messages to this number
@ -94,7 +94,7 @@
class="form-control"
id="contact-email"
placeholder="your.email@example.com"
v-model="modelValue.contact.email"
v-model="modelValue.reporter.email"
/>
<div class="form-text">
We'll send you a confirmation and any updates about this request
@ -125,10 +125,9 @@
import { ref } from "vue";
import { router } from "@/rmo/router";
import type { District } from "@/type/api";
import type { District, PublicReport } from "@/type/api";
import HeaderCompliance from "@/rmo/components/HeaderCompliance.vue";
import ProgressBarCompliance from "@/rmo/components/ProgressBarCompliance.vue";
import type { Compliance } from "@/rmo/view/Compliance.vue";
export interface Contact {
name: string;
@ -138,11 +137,11 @@ export interface Contact {
}
interface Emits {
(e: "doContact"): void;
(e: "update:modelValue", value: Compliance): void;
(e: "update:modelValue", value: PublicReport): void;
}
interface Props {
district: District;
modelValue: Compliance;
modelValue: PublicReport;
}
const emit = defineEmits<Emits>();
const props = defineProps<Props>();

View file

@ -110,7 +110,7 @@
<!-- Upload Area -->
<div class="mb-4">
<label class="form-label fw-semibold">Photos</label>
<ImageUpload v-model="modelValue.images" />
<ImageUpload v-model="images" />
</div>
<!-- Photo Previews -->
@ -166,25 +166,25 @@
import { ref } from "vue";
import { router } from "@/rmo/router";
import type { District } from "@/type/api";
import type { District, PublicReportCompliance } from "@/type/api";
import HeaderCompliance from "@/rmo/components/HeaderCompliance.vue";
import ImageUpload, { Image } from "@/components/ImageUpload.vue";
import ProgressBarCompliance from "@/rmo/components/ProgressBarCompliance.vue";
import type { Compliance } from "@/rmo/view/Compliance.vue";
interface Emits {
(e: "update:modelValue", value: Compliance): void;
(e: "doEvidence"): void;
(e: "update:modelValue", value: PublicReportCompliance): void;
(e: "doEvidence", images: Image[]): void;
}
interface Props {
district: District;
modelValue: Compliance;
modelValue: PublicReportCompliance;
}
const emit = defineEmits<Emits>();
const images = ref<Image[]>([]);
const props = defineProps<Props>();
function doContinue() {
emit("update:modelValue", props.modelValue);
emit("doEvidence");
emit("doEvidence", images.value);
router.push("./permission");
}
</script>

View file

@ -84,7 +84,7 @@
type="radio"
id="access-allowed"
:value="PermissionAccess.GRANTED"
v-model="modelValue.permission.access"
v-model="modelValue.access"
class="mt-1"
/>
<label for="access-allowed">
@ -99,8 +99,7 @@
<!-- Conditional fields for Option 1 -->
<div
v-if="
modelValue.permission.access &&
modelValue.permission.access == PermissionAccess.GRANTED
modelValue.access && modelValue.access == PermissionAccess.GRANTED
"
class="conditional-section"
>
@ -115,7 +114,7 @@
name="access_instructions"
placeholder="Example: Side gate on left, backyard near shed..."
rows="3"
v-model="modelValue.permission.access_instructions"
v-model="modelValue.access_instructions"
></textarea>
</div>
@ -130,7 +129,7 @@
name="gate_code"
placeholder="Enter code if applicable"
type="text"
v-model="modelValue.permission.gate_code"
v-model="modelValue.gate_code"
/>
</div>
@ -141,7 +140,7 @@
type="checkbox"
name="has_dog"
id="has_dog"
v-model="modelValue.permission.has_dog"
v-model="modelValue.has_dog"
/>
<label class="form-check-label" for="has_dog"
>Dog on Property?</label
@ -149,7 +148,7 @@
</div>
</div>
<div class="dog-warning" v-if="modelValue.permission.has_dog">
<div class="dog-warning" v-if="modelValue.has_dog">
<small>
<i class="bi bi-exclamation-triangle"></i>
<strong>Important:</strong> Our staff will only enter if the dog
@ -166,7 +165,7 @@
type="radio"
id="access-with-owner"
:value="PermissionAccess.WITH_OWNER"
v-model="modelValue.permission.access"
v-model="modelValue.access"
class="mt-1"
/>
<label for="access-with-owner">
@ -182,8 +181,8 @@
<div
class="conditional-section"
v-if="
modelValue.permission.access &&
modelValue.permission.access == PermissionAccess.WITH_OWNER
modelValue.access &&
modelValue.access == PermissionAccess.WITH_OWNER
"
>
<div class="form-check mb-3">
@ -192,7 +191,7 @@
type="checkbox"
id="request_scheduled"
name="request_scheduled"
v-model="modelValue.permission.wants_scheduled"
v-model="modelValue.wants_scheduled"
/>
<label class="form-check-label" for="request_scheduled">
I would like to request a scheduled visit
@ -221,7 +220,7 @@
type="radio"
id="access-denied"
:value="PermissionAccess.DENIED"
v-model="modelValue.permission.access"
v-model="modelValue.access"
class="mt-1"
/>
<label for="access-denied">
@ -237,8 +236,7 @@
<div
class="conditional-section"
v-if="
modelValue.permission.access &&
modelValue.permission.access == PermissionAccess.DENIED
modelValue.access && modelValue.access == PermissionAccess.DENIED
"
>
<div class="encouragement-box">
@ -284,24 +282,19 @@ import { ref } from "vue";
import { router } from "@/rmo/router";
import HeaderCompliance from "@/rmo/components/HeaderCompliance.vue";
import ProgressBarCompliance from "@/rmo/components/ProgressBarCompliance.vue";
import { type District, PermissionAccess } from "@/type/api";
import type { Compliance } from "@/rmo/view/Compliance.vue";
import {
type District,
PermissionAccess,
PublicReportCompliance,
} from "@/type/api";
interface Emits {
(e: "doPermission"): void;
(e: "update:modelValue", value: Compliance): void;
}
export interface Permission {
access?: PermissionAccess;
access_instructions: string;
availability_notes: string;
gate_code: string;
has_dog: boolean;
wants_scheduled: boolean;
(e: "update:modelValue", value: PublicReportCompliance): void;
}
interface Props {
district: District;
modelValue: Compliance;
modelValue: PublicReportCompliance;
}
const emit = defineEmits<Emits>();
const props = defineProps<Props>();

View file

@ -155,25 +155,21 @@
<div class="summary-value">
<span
class="status-badge status-provided"
v-if="modelValue.permission?.access == PermissionAccess.GRANTED"
v-if="modelValue.access == PermissionAccess.GRANTED"
>
<i class="bi bi-check-circle"></i> Entry permitted without owner
present
</span>
<span
class="status-badge status-provided"
v-else-if="
modelValue.permission?.access == PermissionAccess.WITH_OWNER
"
v-else-if="modelValue.access == PermissionAccess.WITH_OWNER"
>
<i class="bi bi-check-circle"></i> Entry permitted with owner
present
</span>
<span
class="status-badge status-not-provided"
v-else-if="
modelValue.permission?.access == PermissionAccess.DENIED
"
v-else-if="modelValue.access == PermissionAccess.DENIED"
>
<i class="bi bi-x-circle"></i> Entry denied
</span>
@ -189,8 +185,8 @@
<h3><i class="bi bi-person"></i> Contact Information</h3>
<div class="summary-item">
<div class="summary-label">Name</div>
<div class="summary-value" v-if="modelValue.contact?.name">
{{ modelValue.contact.name }}
<div class="summary-value" v-if="modelValue.reporter?.name">
{{ modelValue.reporter.name }}
</div>
<div class="summary-value status-badge status-not-provided" v-else>
<i class="bi bi-x-circle"></i> Not provided
@ -198,9 +194,9 @@
</div>
<div class="summary-item">
<div class="summary-label">Phone</div>
<div class="summary-value" v-if="modelValue.contact?.phone">
{{ modelValue.contact.phone }}
<small class="text-muted" v-if="modelValue.contact?.can_text"
<div class="summary-value" v-if="modelValue.reporter?.phone">
{{ modelValue.reporter.phone }}
<small class="text-muted" v-if="modelValue.reporter?.can_text"
>(texting OK)</small
>
</div>
@ -210,8 +206,8 @@
</div>
<div class="summary-item">
<div class="summary-label">Email</div>
<div class="summary-value" v-if="modelValue.contact?.email">
{{ modelValue.contact?.email }}
<div class="summary-value" v-if="modelValue.reporter?.email">
{{ modelValue.reporter?.email }}
</div>
<div class="summary-value status-badge status-not-provided" v-else>
<i class="bi bi-x-circle"></i> Not provided
@ -244,14 +240,17 @@
import { router } from "@/rmo/router";
import HeaderCompliance from "@/rmo/components/HeaderCompliance.vue";
import ProgressBarCompliance from "@/rmo/components/ProgressBarCompliance.vue";
import { type District, PermissionAccess } from "@/type/api";
import type { Compliance } from "@/rmo/view/Compliance.vue";
import {
type District,
PermissionAccess,
PublicReportCompliance,
} from "@/type/api";
interface Emits {
(e: "doSubmit"): void;
}
interface Props {
modelValue: Compliance;
modelValue: PublicReportCompliance;
district: District;
}
const emit = defineEmits<Emits>();

View file

@ -29,7 +29,7 @@ body > .container-fluid {
@doEvidence="doEvidence"
@doContact="doContact"
@doPermission="doPermission"
v-model="compliance"
v-model="report"
/>
</LoadingOverlay>
</router-view>
@ -49,66 +49,25 @@ import { useStoreLocal } from "@/store/local";
import { useStoreLocation } from "@/store/location";
import Intro from "@/rmo/content/compliance/Intro.vue";
import LoadingOverlay from "@/components/LoadingOverlay.vue";
import type { District, PublicReport } from "@/type/api";
import {
type ComplianceUpdate,
type District,
PublicReport,
PublicReportCompliance,
} from "@/type/api";
import { Address, Location, PermissionAccess } from "@/type/api";
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;
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;
}
const districtStore = useStoreDistrict();
const compliance = ref<Compliance>({
address: new Address(),
comments: "",
contact: {
name: "",
phone: "",
can_text: true,
email: "",
},
id: "",
images: [],
location: {
latitude: 0,
longitude: 0,
},
permission: {
access: PermissionAccess.UNSELECTED,
access_instructions: "",
availability_notes: "",
gate_code: "",
has_dog: false,
wants_scheduled: false,
},
uri: "",
});
const isLoading = ref<boolean>(true);
const isUploading = ref<boolean>(false);
const props = defineProps<Props>();
const report = ref<PublicReport | null>();
const report = ref<PublicReportCompliance>(new PublicReportCompliance());
const district = computedAsync(async (): Promise<District | undefined> => {
const districts = await districtStore.list();
return districts.find((district: District) => district.slug == props.slug);
@ -139,22 +98,43 @@ async function createReport(client_id: string, loc?: GeolocationPosition) {
}
const body = await resp.json();
storeLocal.setExistingComplianceReportURI(body.uri);
compliance.value.uri = body.uri;
report.value!.uri = body.uri;
}
function doAddress() {
console.log("address done", compliance.value.address);
if (!report.value) {
console.log("can't do address, null report");
return;
}
console.log("address done", report.value.address);
updateReport({
address: compliance.value.address,
address: report.value.address,
});
}
function doEvidence() {
uploadImages(compliance.value.images);
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() {
console.log("contact", compliance.value.contact);
if (!report.value) {
console.log("can't do contact, null report");
return;
}
console.log("contact", report.value.reporter);
}
function doPermission() {
console.log("permission", compliance.value.permission);
if (!report.value) {
console.log("can't do permission, null report");
return;
}
console.log("permission", report.value);
}
async function fetchExistingReport(report_uri: string) {
isLoading.value = true;
@ -171,17 +151,17 @@ async function fetchExistingReport(report_uri: string) {
return;
}
const body = await resp.json();
compliance.value.id = body.id;
compliance.value.uri = body.uri;
compliance.value.address = body.address;
report.value.id = body.id;
report.value.uri = body.uri;
report.value.address = body.address;
isLoading.value = false;
}
async function updateReport(updates: ComplianceUpdate) {
if (!compliance.value.uri) {
if (!report.value.uri) {
console.log("Refusing to update report without URI");
return;
}
const resp = await fetch(compliance.value.uri, {
const resp = await fetch(report.value.uri, {
method: "PUT",
body: JSON.stringify(updates),
headers: {
@ -200,7 +180,7 @@ async function uploadImages(images: Image[]) {
images.map(async (image, index) => {
formData.append(`image[${index}]`, image.file, image.name);
});
const url = `${compliance.value.uri}/image`;
const url = `${report.value.uri}/image`;
const response = await fetch(url, {
body: formData,
method: "POST",
@ -231,9 +211,9 @@ onMounted(() => {
storeLocation
.get()
.then((loc: GeolocationPosition) => {
compliance.value.location = loc.coords;
report.value.location = loc.coords;
updateReport({
location: compliance.value.location,
location: report.value.location,
});
})
.catch((e) => {

View file

@ -94,8 +94,8 @@
</div>
</div>
<NuisanceReportDetail
:nuisance="report.nuisance"
v-if="report.nuisance"
:nuisance="report as PublicReportNuisance"
v-if="report instanceof PublicReportNuisance"
/>
</div>
</div>
@ -151,7 +151,7 @@ import NuisanceReportDetail from "@/rmo/components/NuisanceReportDetail.vue";
import { useStoreDistrict } from "@/rmo/store/district";
import { useStorePublicReport } from "@/store/publicreport";
import type { Marker } from "@/types";
import type { District, PublicReport } from "@/type/api";
import { type District, PublicReport, PublicReportNuisance } from "@/type/api";
import { formatReportID, formatTimeRelative } from "@/format";
// Props

View file

@ -22,10 +22,29 @@ export interface Bounds {
min: Location;
max: Location;
}
export interface Contact {
export interface ContactOptions {
can_text?: boolean;
email?: string;
has_email: boolean;
has_phone: boolean;
name?: string;
phone?: string;
}
export class Contact {
can_text?: boolean;
email?: string;
has_email: boolean;
has_phone: boolean;
name?: string;
phone?: string;
constructor(options?: ContactOptions) {
this.can_text = options?.can_text ?? false;
this.email = options?.email ?? "";
this.has_email = options?.has_email ?? false;
this.has_phone = options?.has_phone ?? false;
this.name = options?.name ?? "";
this.phone = options?.phone ?? "";
}
}
export interface District {
name: string;
@ -114,7 +133,124 @@ export interface Image {
url_content: string;
uuid: string;
}
export interface Nuisance {
export interface ComplianceUpdate {
address?: Address;
comments?: string;
contact?: Contact;
//id: string;
//images?: Image[];
location?: Location;
permission?: Permissions;
//uri: string;
}
export interface PublicReportDTO {
address: Address;
//compliance?: PublicReportCompliance;
created: string;
district: string;
id: string;
images: Image[];
location: Location;
log: LogEntryDTO[];
//nuisance?: Nuisance;
reporter: Contact;
status: string;
type: string;
//water?: Water;
uri: string;
}
export interface PublicReportOptions {
address: Address;
created: Date;
district: string;
id: string;
images: Image[];
location: Location;
log: LogEntry[];
reporter: Contact;
status: string;
type: string;
uri: string;
}
export interface PublicReportComplianceDTO {
access?: PermissionAccess;
access_instructions: string;
availability_notes: string;
comments: string;
gate_code: string;
has_dog: boolean;
wants_scheduled: boolean;
}
export class PublicReport {
address: Address;
created: Date;
district: string;
id: string;
images: Image[];
log: LogEntry[];
reporter: Contact;
status: string;
type: string;
uri: string;
location?: Location;
constructor(options?: PublicReportOptions) {
this.address = options?.address ?? new Address();
this.created = options?.created ?? new Date();
this.district = options?.district ?? "";
this.id = options?.id ?? "";
this.images = options?.images ?? [];
this.log = options?.log ?? [];
this.reporter = options?.reporter ?? new Contact();
this.status = options?.status ?? "";
this.type = options?.type ?? "";
this.uri = options?.uri ?? "";
this.location = options?.location ?? new Location();
}
static fromJSON(json: PublicReportDTO): PublicReport {
return new PublicReport({
address: json.address,
created: new Date(json.created),
district: json.district,
id: json.id,
images: json.images,
location: json.location,
log: json.log.map((l: LogEntryDTO) => LogEntry.fromJSON(l)),
reporter: json.reporter,
status: json.status,
type: json.type,
uri: json.uri,
});
}
}
export interface PublicReportComplianceOptions extends PublicReportOptions {
access?: PermissionAccess;
access_instructions: string;
availability_notes: string;
comments: string;
gate_code: string;
has_dog: boolean;
wants_scheduled: boolean;
}
export class PublicReportCompliance extends PublicReport {
access?: PermissionAccess;
access_instructions: string;
availability_notes: string;
comments: string;
gate_code: string;
has_dog: boolean;
wants_scheduled: boolean;
constructor(options?: PublicReportComplianceOptions) {
super(options);
this.access = options?.access;
this.access_instructions = options?.access_instructions ?? "";
this.availability_notes = options?.availability_notes ?? "";
this.comments = options?.comments ?? "";
this.gate_code = options?.gate_code ?? "";
this.has_dog = options?.has_dog ?? false;
this.wants_scheduled = options?.wants_scheduled ?? false;
}
}
export interface PublicReportNuisanceOptions extends PublicReportOptions {
additional_info: string;
duration: string;
is_location_backyard: boolean;
@ -131,7 +267,43 @@ export interface Nuisance {
time_of_day_evening: boolean;
time_of_day_night: boolean;
}
export interface Water {
export class PublicReportNuisance extends PublicReport {
additional_info: string;
duration: string;
is_location_backyard: boolean;
is_location_frontyard: boolean;
is_location_garden: boolean;
is_location_other: boolean;
is_location_pool: boolean;
source_container: boolean;
source_description: string;
source_gutter: boolean;
source_stagnant: boolean;
time_of_day_day: boolean;
time_of_day_early: boolean;
time_of_day_evening: boolean;
time_of_day_night: boolean;
constructor(options: PublicReportNuisanceOptions) {
super(options);
this.additional_info = options.additional_info;
this.duration = options.duration;
this.is_location_backyard = options.is_location_backyard;
this.is_location_frontyard = options.is_location_frontyard;
this.is_location_garden = options.is_location_garden;
this.is_location_other = options.is_location_other;
this.is_location_pool = options.is_location_pool;
this.source_container = options.source_container;
this.source_description = options.source_description;
this.source_gutter = options.source_gutter;
this.source_stagnant = options.source_stagnant;
this.time_of_day_day = options.time_of_day_day;
this.time_of_day_early = options.time_of_day_early;
this.time_of_day_evening = options.time_of_day_evening;
this.time_of_day_night = options.time_of_day_night;
}
}
export interface PublicReportWaterOptions extends PublicReportOptions {
access_comments: string;
access_gate: boolean;
access_fence: boolean;
@ -147,55 +319,65 @@ export interface Water {
is_reporter_owner: boolean;
owner: Contact;
}
export interface PublicReportDTO {
address: Address;
created: string;
district: string;
id: string;
images: Image[];
location: Location;
log: LogEntryDTO[];
nuisance: Nuisance;
reporter: Contact;
status: string;
type: string;
water: Water;
uri: string;
}
export class PublicReport {
constructor(
public address: Address,
public created: Date,
public district: string,
public id: string,
public images: Image[],
public log: LogEntry[],
public reporter: Contact,
public status: string,
public type: string,
public uri: string,
public location?: Location,
public nuisance?: Nuisance,
public water?: Water,
) {}
static fromJSON(json: PublicReportDTO): PublicReport {
return new PublicReport(
json.address,
new Date(json.created),
json.district,
json.id,
json.images,
json.log.map((l: LogEntryDTO) => LogEntry.fromJSON(l)),
json.reporter,
json.status,
json.type,
json.uri,
json.location,
json.nuisance,
json.water,
);
export class PublicReportWater extends PublicReport {
access_comments: string;
access_gate: boolean;
access_fence: boolean;
access_locked: boolean;
access_dog: boolean;
access_other: boolean;
comments: string;
has_adult: boolean;
has_backyard_permission: boolean;
has_larvae: boolean;
has_pupae: boolean;
is_reporter_confidential: boolean;
is_reporter_owner: boolean;
owner: Contact;
constructor(options: PublicReportWaterOptions) {
super(options);
this.access_comments = options.access_comments;
this.access_gate = options.access_gate;
this.access_fence = options.access_fence;
this.access_locked = options.access_locked;
this.access_dog = options.access_dog;
this.access_other = options.access_other;
this.comments = options.comments;
this.has_adult = options.has_adult;
this.has_backyard_permission = options.has_backyard_permission;
this.has_larvae = options.has_larvae;
this.has_pupae = options.has_pupae;
this.is_reporter_confidential = options.is_reporter_confidential;
this.is_reporter_owner = options.is_reporter_owner;
this.owner = options.owner;
}
}
/*
address: new Address(),
comments: "",
contact: {
name: "",
phone: "",
can_text: true,
email: "",
},
id: "",
images: [],
location: {
latitude: 0,
longitude: 0,
},
permission: {
access: PermissionAccess.UNSELECTED,
access_instructions: "",
availability_notes: "",
gate_code: "",
has_dog: false,
wants_scheduled: false,
},
uri: "",
});
*/
export interface CommunicationDTO {
created: string;
id: string;