forked from jsnbuchanan/crowd-funder-for-time-pwa
Merge pull request 'record & list offers on a project' (#83) from many-misc into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#83
This commit is contained in:
317
src/components/OfferDialog.vue
Normal file
317
src/components/OfferDialog.vue
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="dialog-overlay">
|
||||||
|
<div class="dialog">
|
||||||
|
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
|
placeholder="Description, prerequisites, terms, etc."
|
||||||
|
v-model="description"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row mb-6">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
||||||
|
>
|
||||||
|
Hours
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="decrement()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
||||||
|
v-model="hours"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="increment()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row mb-6">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
||||||
|
>
|
||||||
|
Expiration
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="w-full border border-slate-400 px-2 py-2 rounded-r"
|
||||||
|
:placeholder="'Date, eg. ' + new Date().toISOString().slice(0, 10)"
|
||||||
|
v-model="expirationDateInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
Sign & Send
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
import { createAndSubmitOffer } from "@/libs/endorserServer";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class OfferDialog extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
@Prop message = "";
|
||||||
|
@Prop projectId = "";
|
||||||
|
|
||||||
|
activeDid = "";
|
||||||
|
apiServer = "";
|
||||||
|
|
||||||
|
description = "";
|
||||||
|
expirationDateInput = "";
|
||||||
|
hours = "0";
|
||||||
|
visible = false;
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log("Error retrieving settings from database:", err);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
err.message ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.close();
|
||||||
|
this.description = "";
|
||||||
|
this.hours = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm() {
|
||||||
|
this.close();
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
text: "Recording the offer...",
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
// this is asynchronous, but we don't need to wait for it to complete
|
||||||
|
this.recordOffer(
|
||||||
|
this.description,
|
||||||
|
parseFloat(this.hours),
|
||||||
|
this.expirationDateInput,
|
||||||
|
).then(() => {
|
||||||
|
this.description = "";
|
||||||
|
this.hours = "0";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = (await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first()) as Account;
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Offer records for DID ${activeDid} but no identity was found",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param description may be an empty string
|
||||||
|
* @param hours may be 0
|
||||||
|
*/
|
||||||
|
public async recordOffer(
|
||||||
|
description?: string,
|
||||||
|
hours?: number,
|
||||||
|
expirationDateInput?: string,
|
||||||
|
) {
|
||||||
|
if (!this.activeDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identity before you can record an offer.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description && !hours) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must enter a description or some number of hours.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
const result = await createAndSubmitOffer(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
identity,
|
||||||
|
description,
|
||||||
|
hours,
|
||||||
|
expirationDateInput,
|
||||||
|
this.projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.type === "error" ||
|
||||||
|
this.isOfferCreationError(result.response)
|
||||||
|
) {
|
||||||
|
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||||
|
console.log("Error with offer creation result:", result);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage || "There was an error creating the offer.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: "That offer was recorded.",
|
||||||
|
},
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("Error with offer recordation caught:", error);
|
||||||
|
const message =
|
||||||
|
error.userMessage ||
|
||||||
|
error.response?.data?.error?.message ||
|
||||||
|
"There was an error recording the offer.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for readability
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param result response "data" from the server
|
||||||
|
* @returns true if the result indicates an error
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
isOfferCreationError(result: any) {
|
||||||
|
return result.status !== 201 || result.data?.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||||
|
* @returns best guess at an error message
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
getOfferCreationErrorMessage(result: any) {
|
||||||
|
return (
|
||||||
|
result.error?.userMessage ||
|
||||||
|
result.error?.error ||
|
||||||
|
result.response?.data?.error?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background-color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -59,17 +59,40 @@ export interface GiveServerRecord {
|
|||||||
unit: string;
|
unit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OfferServerRecord {
|
||||||
|
amount: number;
|
||||||
|
amountGiven: number;
|
||||||
|
offeredByDid: string;
|
||||||
|
recipientDid: string;
|
||||||
|
requirementsMet: boolean;
|
||||||
|
unit: string;
|
||||||
|
validThrough: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GiveVerifiableCredential {
|
export interface GiveVerifiableCredential {
|
||||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||||
"@type": string;
|
"@type": "GiveAction";
|
||||||
agent?: { identifier: string };
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
fulfills?: { "@type": string; identifier: string };
|
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string };
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
object?: { amountOfThisGood: number; unitCode: string };
|
object?: { amountOfThisGood: number; unitCode: string };
|
||||||
recipient?: { identifier: string };
|
recipient?: { identifier: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OfferVerifiableCredential {
|
||||||
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||||
|
"@type": "Offer";
|
||||||
|
description?: string;
|
||||||
|
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||||
|
itemOffered?: {
|
||||||
|
description?: string;
|
||||||
|
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
||||||
|
};
|
||||||
|
offeredBy?: { identifier: string };
|
||||||
|
validThrough?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlanVerifiableCredential {
|
export interface PlanVerifiableCredential {
|
||||||
"@context": "https://schema.org";
|
"@context": "https://schema.org";
|
||||||
"@type": "PlanAction";
|
"@type": "PlanAction";
|
||||||
@@ -152,7 +175,7 @@ export interface ErrorResult {
|
|||||||
error: InternalError;
|
error: InternalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
|
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
@@ -172,20 +195,81 @@ export async function createAndSubmitGive(
|
|||||||
description?: string,
|
description?: string,
|
||||||
hours?: number,
|
hours?: number,
|
||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
): Promise<CreateAndSubmitGiveResult> {
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
try {
|
const vcClaim: GiveVerifiableCredential = {
|
||||||
const vcClaim: GiveVerifiableCredential = {
|
"@context": "https://schema.org",
|
||||||
"@context": "https://schema.org",
|
"@type": "GiveAction",
|
||||||
"@type": "GiveAction",
|
recipient: toDid ? { identifier: toDid } : undefined,
|
||||||
recipient: toDid ? { identifier: toDid } : undefined,
|
agent: fromDid ? { identifier: fromDid } : undefined,
|
||||||
agent: fromDid ? { identifier: fromDid } : undefined,
|
description: description || undefined,
|
||||||
description: description || undefined,
|
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
|
||||||
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
|
fulfills: fulfillsProjectHandleId
|
||||||
fulfills: fulfillsProjectHandleId
|
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
||||||
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
: undefined,
|
||||||
: undefined,
|
};
|
||||||
};
|
return createAndSubmitClaim(
|
||||||
|
vcClaim as GenericClaim,
|
||||||
|
identity,
|
||||||
|
apiServer,
|
||||||
|
axios,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
|
*
|
||||||
|
* @param identity
|
||||||
|
* @param description may be null; should have this or hours
|
||||||
|
* @param hours may be null; should have this or description
|
||||||
|
* @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
|
||||||
|
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
||||||
|
*/
|
||||||
|
export async function createAndSubmitOffer(
|
||||||
|
axios: Axios,
|
||||||
|
apiServer: string,
|
||||||
|
identity: IIdentifier,
|
||||||
|
description?: string,
|
||||||
|
hours?: number,
|
||||||
|
expirationDate?: string,
|
||||||
|
fulfillsProjectHandleId?: string,
|
||||||
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
|
const vcClaim: OfferVerifiableCredential = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Offer",
|
||||||
|
offeredBy: { identifier: identity.did },
|
||||||
|
validThrough: expirationDate || undefined,
|
||||||
|
};
|
||||||
|
if (hours) {
|
||||||
|
vcClaim.includesObject = {
|
||||||
|
amountOfThisGood: hours,
|
||||||
|
unitCode: "HUR",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (description) {
|
||||||
|
vcClaim.itemOffered = { description };
|
||||||
|
}
|
||||||
|
if (fulfillsProjectHandleId) {
|
||||||
|
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
||||||
|
vcClaim.itemOffered.isPartOf = {
|
||||||
|
"@type": "PlanAction",
|
||||||
|
identifier: fulfillsProjectHandleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return createAndSubmitClaim(
|
||||||
|
vcClaim as GenericClaim,
|
||||||
|
identity,
|
||||||
|
apiServer,
|
||||||
|
axios,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAndSubmitClaim(
|
||||||
|
vcClaim: GenericClaim,
|
||||||
|
identity: IIdentifier,
|
||||||
|
apiServer: string,
|
||||||
|
axios: Axios,
|
||||||
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
|
try {
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||||
@@ -226,15 +310,11 @@ export async function createAndSubmitGive(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { type: "success", response };
|
return { type: "success", response };
|
||||||
} catch (error: unknown) {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("Error creating claim:", error);
|
||||||
const errorMessage: string =
|
const errorMessage: string =
|
||||||
error === null
|
error.response?.data?.error?.message || error.message || "Unknown error";
|
||||||
? "Null error"
|
|
||||||
: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: typeof error === "object" && error !== null && "message" in error
|
|
||||||
? (error as { message: string }).message
|
|
||||||
: "Unknown error";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -80,10 +80,21 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div v-if="activeDid" class="text-center">
|
||||||
|
<button
|
||||||
|
@click="openOfferDialog({ name: 'you', did: activeDid })"
|
||||||
|
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
I offer…
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div v-if="activeDid" class="text-center">
|
<div v-if="activeDid" class="text-center">
|
||||||
<button
|
<button
|
||||||
@click="openDialog({ name: 'you', did: activeDid })"
|
@click="openGiftDialog({ name: 'you', did: activeDid })"
|
||||||
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
I gave…
|
I gave…
|
||||||
@@ -93,7 +104,7 @@
|
|||||||
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
|
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
|
||||||
|
|
||||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
<li @click="openDialog()">
|
<li @click="openGiftDialog()">
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:entityId="null"
|
:entityId="null"
|
||||||
:iconSize="64"
|
:iconSize="64"
|
||||||
@@ -108,7 +119,7 @@
|
|||||||
<li
|
<li
|
||||||
v-for="contact in allContacts"
|
v-for="contact in allContacts"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
@click="openDialog(contact)"
|
@click="openGiftDialog(contact)"
|
||||||
>
|
>
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:entityId="contact.did"
|
:entityId="contact.did"
|
||||||
@@ -134,7 +145,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gifts to & from this -->
|
<!-- Gifts to & from this -->
|
||||||
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
|
Offered To This Project
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div v-if="offersToThis.length === 0">
|
||||||
|
(None yet. Record one above.)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
v-for="offer in offersToThis"
|
||||||
|
:key="offer.id"
|
||||||
|
class="py-1.5 border-b border-slate-300"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span>
|
||||||
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ didInfo(offer.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="offer.amount">
|
||||||
|
<fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ offer.amount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="offer.objectDescription" class="text-slate-500">
|
||||||
|
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ offer.objectDescription }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
Given To This Project
|
Given To This Project
|
||||||
@@ -199,11 +243,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiftedDialog
|
<GiftedDialog
|
||||||
ref="customDialog"
|
ref="customGiveDialog"
|
||||||
message="Received from"
|
message="Received from"
|
||||||
:projectId="this.projectId"
|
:projectId="this.projectId"
|
||||||
>
|
>
|
||||||
</GiftedDialog>
|
</GiftedDialog>
|
||||||
|
<OfferDialog ref="customOfferDialog" :projectId="this.projectId">
|
||||||
|
</OfferDialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -214,6 +260,7 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import OfferDialog from "@/components/OfferDialog.vue";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
@@ -222,6 +269,7 @@ import {
|
|||||||
didInfo,
|
didInfo,
|
||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
|
OfferServerRecord,
|
||||||
PlanServerRecord,
|
PlanServerRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
@@ -236,7 +284,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
|
||||||
})
|
})
|
||||||
export default class ProjectViewView extends Vue {
|
export default class ProjectViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -250,10 +298,11 @@ export default class ProjectViewView extends Vue {
|
|||||||
fulfilledByThis: PlanServerRecord | null = null;
|
fulfilledByThis: PlanServerRecord | null = null;
|
||||||
fulfillersToThis: Array<PlanServerRecord> = [];
|
fulfillersToThis: Array<PlanServerRecord> = [];
|
||||||
givesToThis: Array<GiveServerRecord> = [];
|
givesToThis: Array<GiveServerRecord> = [];
|
||||||
|
issuer = "";
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
name = "";
|
name = "";
|
||||||
issuer = "";
|
offersToThis: Array<OfferServerRecord> = [];
|
||||||
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
||||||
timeSince = "";
|
timeSince = "";
|
||||||
truncatedDesc = "";
|
truncatedDesc = "";
|
||||||
@@ -433,6 +482,42 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const offersToUrl =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/offersToPlans?planIds=" +
|
||||||
|
encodeURIComponent(JSON.stringify([projectId]));
|
||||||
|
try {
|
||||||
|
const resp = await this.axios.get(offersToUrl, { headers });
|
||||||
|
if (resp.status === 200 && resp.data.data) {
|
||||||
|
this.offersToThis = resp.data.data;
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to retrieve offers to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving offers to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Error retrieving offers to this project:",
|
||||||
|
serverError.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const fulfilledByUrl =
|
const fulfilledByUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
||||||
@@ -533,8 +618,12 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
openDialog(contact: GiverInputInfo) {
|
openGiftDialog(contact: GiverInputInfo) {
|
||||||
(this.$refs.customDialog as GiftedDialog).open(contact);
|
(this.$refs.customGiveDialog as GiftedDialog).open(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
openOfferDialog() {
|
||||||
|
(this.$refs.customOfferDialog as OfferDialog).open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user