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:
parent
c92d23792f
commit
ffb981e40b
7 changed files with 180 additions and 10 deletions
3
ts/components/CardEmail.vue
Normal file
3
ts/components/CardEmail.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<p>fake email card</p>
|
||||
</template>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
32
ts/components/CardResource.vue
Normal file
32
ts/components/CardResource.vue
Normal 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>
|
||||
3
ts/components/CardText.vue
Normal file
3
ts/components/CardText.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<p>fake text card</p>
|
||||
</template>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
110
ts/store/resource.ts
Normal 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,
|
||||
),
|
||||
};
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue