Add generic resource store, start adding context cards

These cards are meant to be generic and can show an unlimited amount of
related context about a communication.
This commit is contained in:
Eli Ribble 2026-05-15 17:10:03 +00:00
parent c92d23792f
commit ffb981e40b
No known key found for this signature in database
7 changed files with 180 additions and 10 deletions

View file

@ -0,0 +1,3 @@
<template>
<p>fake email card</p>
</template>

View file

@ -12,7 +12,10 @@
}
</style>
<template>
<div class="details-section p-3 border-top">
<div v-if="!report">
<p>loading...</p>
</div>
<div class="details-section p-3 border-top" v-else>
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 class="mb-1">
@ -141,13 +144,14 @@
</template>
<script setup lang="ts">
import { computed } from "vue";
import { computedAsync } from "@vueuse/core";
import MapMultipoint from "@/components/MapMultipoint.vue";
import TimeRelative from "@/components/TimeRelative.vue";
import PublicReportCardCompliance from "@/components/PublicReportCardCompliance.vue";
import PublicReportCardNuisance from "@/components/PublicReportCardNuisance.vue";
import PublicReportCardWater from "@/components/PublicReportCardWater.vue";
import { formatAddress } from "@/format";
import { useStoreResource } from "@/store/resource";
import {
PublicReport,
PublicReportCompliance,
@ -159,10 +163,15 @@ interface Emits {
(e: "viewImage", index: number): void;
}
interface Props {
report: PublicReport;
reportURI: string;
}
const emit = defineEmits<Emits>();
const props = defineProps<Props>();
const storeResource = useStoreResource();
const report = computedAsync(() => {
return storeResource.publicreport.byURI(props.reportURI);
});
function openPhotoViewer(index: number) {
emit("viewImage", index);
}

View file

@ -0,0 +1,32 @@
<template>
<CardEmail v-if="isEmail()" :emailURI="resource.uri" />
<CardPublicReport
v-if="isPublicReport()"
:reportURI="resource.uri"
@viewImage="doViewImage"
/>
<CardText v-if="isText()" :textURI="resource.uri" />
</template>
<script setup lang="ts">
import { ResourceStub } from "@/type/api";
import CardEmail from "@/components/CardEmail.vue";
import CardPublicReport from "@/components/CardPublicReport.vue";
import CardText from "@/components/CardText.vue";
interface Props {
resource: ResourceStub;
}
const props = defineProps<Props>();
function isEmail(): boolean {
return props.resource.type == "email";
}
function isPublicReport(): boolean {
return props.resource.type.startsWith("publicreport.");
}
function isText(): boolean {
return props.resource.type == "text";
}
function doViewImage() {
console.log("doViewImage");
}
</script>

View file

@ -0,0 +1,3 @@
<template>
<p>fake text card</p>
</template>

View file

@ -74,9 +74,11 @@
</div>
<div class="h-100 d-flex flex-column" v-else>
<ResourceCard
<CardResource
:resource="resource"
v-for="(resource, index) in selectedCommunication.related"
v-for="(resource, index) in sortedByCreated(
selectedCommunication.related,
)"
/>
</div>
</div>
@ -86,14 +88,20 @@
<script setup lang="ts">
import { computed } from "vue";
import ResourceCard from "@/components/ResourceCard.vue";
import CardResource from "@/components/CardResource.vue";
import TimeRelative from "@/components/TimeRelative.vue";
import Map, { LngLatBounds } from "@/map/Map.vue";
import Layer from "@/map/Layer.vue";
import Source from "@/map/Source.vue";
import { useSessionStore } from "@/store/session";
import type { Marker } from "@/types";
import type { Bounds, Communication, PublicReport, User } from "@/type/api";
import type {
Bounds,
Communication,
PublicReport,
ResourceStub,
User,
} from "@/type/api";
interface Emits {
(e: "viewImage", index: number): void;
@ -112,4 +120,9 @@ const session = useSessionStore();
function openPhotoViewer(index: number) {
emit("viewImage", index);
}
function sortedByCreated(resources: ResourceStub[]): ResourceStub[] {
return resources.sort((a: ResourceStub, b: ResourceStub): number => {
return a.created.getTime() - b.created.getTime();
});
}
</script>

View file

@ -5,10 +5,10 @@
<FlyoverPoolCard :location="signal.location" :markers="[]" />
</div>
<div v-else-if="signal.type == 'publicreport nuisance' && signal.report">
<PublicReportCard :report="signal.report" />
<CardPublicReport :reportURI="signal.report.uri" />
</div>
<div v-else-if="signal.type == 'publicreport water' && signal.report">
<PublicReportCard :report="signal.report" />
<CardPublicReport :reportURI="signal.report.uri" />
</div>
<div v-else>
<p>No report or pool</p>
@ -17,7 +17,7 @@
<script setup lang="ts">
import FlyoverPoolCard from "@/components/FlyoverPoolCard.vue";
import PublicReportCard from "@/components/PublicReportCard.vue";
import CardPublicReport from "@/components/CardPublicReport.vue";
import TimeRelative from "@/components/TimeRelative.vue";
import { formatAddressShort } from "@/format";
import { Signal } from "@/type/api";

110
ts/store/resource.ts Normal file
View file

@ -0,0 +1,110 @@
import { defineStore } from "pinia";
import { shallowRef } from "vue";
import { SSEManager, SSEMessageResource } from "@/SSEManager";
import { useSessionStore } from "@/store/session";
import { apiClient } from "@/client";
import {
Communication,
type CommunicationDTO,
PublicReport,
type PublicReportDTO,
} from "@/type/api";
interface uriHaver {
uri: string;
}
type jsonConverter<dto, full> = (arg: dto) => full;
function createResourceStore<dto, full extends uriHaver>(
resource_name: string,
api_base: string,
from_json: jsonConverter<dto, full>,
) {
const _resourceByURI = shallowRef<Map<string, full>>(new Map());
const _resourceFetchByURI = shallowRef<Map<string, Promise<full> | null>>(
new Map(),
);
// Subscription
SSEManager.subscribe((msg: SSEMessageResource) => {
if (msg.resource.startsWith(resource_name) && msg.type == "updated") {
fetchByURI(msg.uri);
}
});
async function byID(id: string): Promise<full> {
const uri = uriFromID(id);
const cur = _resourceFetchByURI.value.get(uri);
if (cur) {
return cur;
}
return fetchByID(id);
}
async function byURI(uri: string): Promise<full> {
const cur = _resourceFetchByURI.value.get(uri);
if (cur) {
return cur;
}
return fetchByURI(uri);
}
async function fetchAll(): Promise<full[]> {
const sessionStore = useSessionStore();
const session = await sessionStore.get();
const params = new URLSearchParams();
params.append("sort", "-created");
const url = `${session.urls.api.mailer}?${params}`;
const dtos = (await apiClient.JSONGet(url)) as dto[];
const resources = dtos.map((m: dto) => from_json(m));
resources.forEach((r: full) => {
_resourceByURI.value.set(r.uri, r);
});
return resources;
}
async function fetchByID(id: string): Promise<full> {
const uri = uriFromID(id);
return fetchByURI(uri);
}
async function fetchByURI(uri: string): Promise<full> {
try {
const response = await fetch(uri);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const body: dto = await response.json();
const report = from_json(body);
_resourceByURI.value.set(report.uri, report);
return report;
} catch (err) {
console.error("Error loading users:", err);
throw err;
}
}
function loadingURI(uri: string): boolean {
return !!_resourceFetchByURI.value.get(uri);
}
function uriFromID(id: string): string {
return `${api_base}/${id}`;
}
return {
byID,
byURI,
fetchAll,
loadingURI,
};
}
export const useStoreResource = defineStore("resource", () => {
return {
communication: createResourceStore<CommunicationDTO, Communication>(
"sync:communication",
"/communication",
Communication.fromJSON,
),
publicreport: createResourceStore<PublicReportDTO, PublicReport>(
"sync:publicreport",
"/publicreport",
PublicReport.fromJSON,
),
};
});