You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
633 lines
17 KiB
633 lines
17 KiB
<template>
|
|
<QuickNav />
|
|
<TopMessage />
|
|
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Back -->
|
|
<div
|
|
v-if="!hideBackButton"
|
|
class="text-lg text-center font-light relative px-7"
|
|
>
|
|
<h1
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
@click="cancelBack()"
|
|
>
|
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Heading -->
|
|
<h1 class="text-4xl text-center font-light px-4 mb-4">What Is Offered</h1>
|
|
|
|
<h1 class="text-xl font-bold text-center mb-4">
|
|
<span>
|
|
Offer to
|
|
{{
|
|
offeredToProject
|
|
? projectName
|
|
: offeredToRecipient
|
|
? recipientName
|
|
: "someone unidentified"
|
|
}}</span
|
|
>
|
|
</h1>
|
|
<textarea
|
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
placeholder="What is offered"
|
|
v-model="descriptionOfItem"
|
|
data-testId="itemDescription"
|
|
/>
|
|
<div class="flex flex-row justify-center">
|
|
<span
|
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
|
|
@click="changeUnitCode()"
|
|
>
|
|
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
|
|
</span>
|
|
<div
|
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
|
@click="amountInput === '0' ? null : decrement()"
|
|
>
|
|
<fa icon="chevron-left" />
|
|
</div>
|
|
<input
|
|
type="number"
|
|
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
|
v-model="amountInput"
|
|
data-testId="inputOfferAmount"
|
|
/>
|
|
<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 mt-2">
|
|
<span
|
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
|
>
|
|
Conditions
|
|
</span>
|
|
<textarea
|
|
class="w-full border border-slate-400 px-3 py-2 rounded-r"
|
|
placeholder="Prerequisites, other people to include, etc."
|
|
v-model="descriptionOfCondition"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-row mt-2">
|
|
<span
|
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
|
>
|
|
{{ validThroughDateInput ? "" : "No" }} Expiration
|
|
</span>
|
|
<input
|
|
v-model="validThroughDateInput"
|
|
type="date"
|
|
class="w-full rounded border border-slate-400 px-3 py-2 rounded-r"
|
|
/>
|
|
</div>
|
|
|
|
<div class="h-7 mt-4 flex">
|
|
<input
|
|
v-if="projectId && !offeredToRecipient"
|
|
type="checkbox"
|
|
class="h-6 w-6 mr-2"
|
|
v-model="offeredToProject"
|
|
/>
|
|
<fa
|
|
v-else
|
|
icon="square"
|
|
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
|
|
@click="notifyUserOfProject()"
|
|
/>
|
|
<label class="text-sm mt-1">
|
|
{{
|
|
projectId
|
|
? "This is offered to " + projectName
|
|
: "No project was chosen"
|
|
}}
|
|
</label>
|
|
</div>
|
|
|
|
<div class="h-7 mt-4 flex">
|
|
<input
|
|
v-if="recipientDid && !offeredToProject"
|
|
type="checkbox"
|
|
class="h-6 w-6 mr-2"
|
|
v-model="offeredToRecipient"
|
|
/>
|
|
<fa
|
|
v-else
|
|
icon="square"
|
|
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
|
|
@click="notifyUserOfRecipient()"
|
|
/>
|
|
<label class="text-sm mt-1">
|
|
{{
|
|
recipientDid
|
|
? "This is offered to " + recipientName
|
|
: "No recipient was chosen."
|
|
}}
|
|
</label>
|
|
</div>
|
|
|
|
<div v-if="showGeneralAdvanced" class="mt-4 flex">
|
|
<router-link
|
|
:to="{
|
|
name: 'claim-add-raw',
|
|
query: {
|
|
claim: constructOfferParam(),
|
|
},
|
|
}"
|
|
class="text-blue-500"
|
|
>
|
|
Edit & Submit Raw
|
|
</router-link>
|
|
</div>
|
|
|
|
<p class="text-center mb-2 mt-6 italic">
|
|
Sign & Send to publish to the world
|
|
<fa
|
|
icon="circle-info"
|
|
class="pl-2 text-blue-500 cursor-pointer"
|
|
@click="explainData()"
|
|
/>
|
|
</p>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
<button
|
|
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
|
@click="confirm"
|
|
>
|
|
Sign & Send
|
|
</button>
|
|
<button
|
|
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
|
@click="cancel"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { Router } from "vue-router";
|
|
|
|
import QuickNav from "@/components/QuickNav.vue";
|
|
import TopMessage from "@/components/TopMessage.vue";
|
|
import { NotificationIface } from "@/constants/app";
|
|
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
|
|
import {
|
|
createAndSubmitOffer,
|
|
didInfo,
|
|
editAndSubmitOffer,
|
|
GenericCredWrapper,
|
|
getPlanFromCache,
|
|
hydrateOffer,
|
|
OfferVerifiableCredential,
|
|
} from "@/libs/endorserServer";
|
|
import * as libsUtil from "@/libs/util";
|
|
import { Contact } from "@/db/tables/contacts";
|
|
|
|
@Component({
|
|
components: {
|
|
QuickNav,
|
|
TopMessage,
|
|
},
|
|
})
|
|
export default class OfferDetailsView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
|
|
amountInput = "0";
|
|
descriptionOfCondition = "";
|
|
descriptionOfItem = "";
|
|
destinationPathAfter = "";
|
|
hideBackButton = false;
|
|
message = "";
|
|
offeredToProject = false;
|
|
offeredToRecipient = false;
|
|
offererDid: string | undefined;
|
|
offerId = "";
|
|
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
|
|
projectId = "";
|
|
projectName = "a project";
|
|
recipientDid = "";
|
|
recipientName = "";
|
|
showGeneralAdvanced = false;
|
|
unitCode = "HUR";
|
|
validThroughDateInput = "";
|
|
|
|
libsUtil = libsUtil;
|
|
|
|
async mounted() {
|
|
try {
|
|
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
|
|
? (JSON.parse(
|
|
(this.$route as Router).query["prevCredToEdit"],
|
|
) as GenericCredWrapper<OfferVerifiableCredential>)
|
|
: undefined;
|
|
} catch (error) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Retrieval Error",
|
|
text: "The previous record isn't available for editing. If you submit, you'll create a new record.",
|
|
},
|
|
6000,
|
|
);
|
|
}
|
|
|
|
const prevAmount =
|
|
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
|
|
this.amountInput =
|
|
(this.$route as Router).query["amountInput"] ||
|
|
(prevAmount ? String(prevAmount) : "") ||
|
|
this.amountInput;
|
|
this.unitCode = ((this.$route as Router).query["unitCode"] ||
|
|
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
|
|
this.unitCode) as string;
|
|
|
|
this.descriptionOfCondition =
|
|
this.prevCredToEdit?.claim?.description || this.descriptionOfCondition;
|
|
this.descriptionOfItem =
|
|
(this.$route as Router).query["description"] ||
|
|
this.prevCredToEdit?.claim?.itemOffered?.description ||
|
|
this.descriptionOfItem;
|
|
this.destinationPathAfter = (this.$route as Router).query[
|
|
"destinationPathAfter"
|
|
];
|
|
this.offererDid = ((this.$route as Router).query["offererDid"] ||
|
|
this.prevCredToEdit?.claim?.agent?.identifier ||
|
|
this.offererDid) as string;
|
|
this.hideBackButton =
|
|
(this.$route as Router).query["hideBackButton"] === "true";
|
|
this.message = ((this.$route as Router).query["message"] as string) || "";
|
|
|
|
// find any project ID
|
|
let project;
|
|
if (
|
|
this.prevCredToEdit?.claim?.itemOffered?.isPartOf?.["@type"] ===
|
|
"PlanAction"
|
|
) {
|
|
project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf;
|
|
}
|
|
this.projectId = ((this.$route as Router).query["projectId"] ||
|
|
project?.identifier ||
|
|
this.projectId) as string;
|
|
this.projectName = ((this.$route as Router).query["projectName"] ||
|
|
project?.name ||
|
|
this.projectName) as string;
|
|
|
|
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
|
|
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
|
|
this.recipientName =
|
|
((this.$route as Router).query["recipientName"] as string) || "";
|
|
|
|
this.validThroughDateInput =
|
|
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
|
|
|
|
try {
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.apiServer = settings.apiServer ?? "";
|
|
this.activeDid = settings.activeDid ?? "";
|
|
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
|
|
|
|
let allContacts: Contact[] = [];
|
|
let allMyDids: string[] = [];
|
|
if (this.recipientDid && !this.recipientName) {
|
|
allContacts = await db.contacts.toArray();
|
|
|
|
await accountsDB.open();
|
|
const allAccounts = await accountsDB.accounts.toArray();
|
|
allMyDids = allAccounts.map((acc) => acc.did);
|
|
this.recipientName = didInfo(
|
|
this.recipientDid,
|
|
this.activeDid,
|
|
allMyDids,
|
|
allContacts,
|
|
);
|
|
}
|
|
// these should be functions but something's wrong with the syntax in the <> conditional
|
|
this.offeredToProject = !!this.projectId;
|
|
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (err: any) {
|
|
console.error("Error retrieving settings from database:", err);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: err.message || "There was an error retrieving your settings.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
if (this.projectId && !this.projectName) {
|
|
// console.log("Getting project name from cache", this.projectId);
|
|
const project = await getPlanFromCache(
|
|
this.projectId,
|
|
this.axios,
|
|
this.apiServer,
|
|
this.activeDid,
|
|
);
|
|
this.projectName = project?.name
|
|
? "the project: " + project.name
|
|
: "a project";
|
|
}
|
|
}
|
|
|
|
changeUnitCode() {
|
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
|
const index = units.indexOf(this.unitCode);
|
|
this.unitCode = units[(index + 1) % units.length];
|
|
}
|
|
|
|
increment() {
|
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
|
}
|
|
|
|
decrement() {
|
|
this.amountInput = `${Math.max(
|
|
0,
|
|
(parseFloat(this.amountInput) || 1) - 1,
|
|
)}`;
|
|
}
|
|
|
|
cancel() {
|
|
if (this.destinationPathAfter) {
|
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
|
} else {
|
|
(this.$router as Router).back();
|
|
}
|
|
}
|
|
|
|
cancelBack() {
|
|
(this.$router as Router).back();
|
|
}
|
|
|
|
async confirm() {
|
|
if (!this.activeDid) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "You must select an identifier before you can record a offer.",
|
|
},
|
|
2000,
|
|
);
|
|
return;
|
|
}
|
|
if (parseFloat(this.amountInput) < 0) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
text: "You may not send a negative number.",
|
|
title: "",
|
|
},
|
|
2000,
|
|
);
|
|
return;
|
|
}
|
|
if (!this.descriptionOfItem && !parseFloat(this.amountInput)) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: `You must enter a description or some number of ${
|
|
this.libsUtil.UNIT_LONG[this.unitCode]
|
|
}.`,
|
|
},
|
|
2000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
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
|
|
await this.recordOffer();
|
|
}
|
|
|
|
notifyUserOfProject() {
|
|
if (!this.projectId) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Error",
|
|
text: "To assign to a project, you must open this page through a project.",
|
|
},
|
|
3000,
|
|
);
|
|
} else {
|
|
// must be because offeredToRecipient is true
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Error",
|
|
text: "You cannot assign both to a project and to a recipient.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
}
|
|
|
|
notifyUserOfRecipient() {
|
|
if (!this.recipientDid) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Error",
|
|
text: "To assign to a recipient, you must open this page from a contact.",
|
|
},
|
|
3000,
|
|
);
|
|
} else {
|
|
// must be because offeredToProject is true
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Error",
|
|
text: "You cannot assign both to a recipient and to a project.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param offererDid may be null
|
|
* @param description may be an empty string
|
|
* @param amountInput may be 0
|
|
* @param unitCode may be omitted, defaults to "HUR"
|
|
*/
|
|
public async recordOffer() {
|
|
try {
|
|
const recipientDid = this.offeredToRecipient
|
|
? this.recipientDid
|
|
: undefined;
|
|
const projectId = this.offeredToProject ? this.projectId : undefined;
|
|
let result;
|
|
if (this.prevCredToEdit) {
|
|
// don't create from a blank one in case some properties were set from a different interface
|
|
result = await editAndSubmitOffer(
|
|
this.axios,
|
|
this.apiServer,
|
|
this.prevCredToEdit,
|
|
this.activeDid,
|
|
this.descriptionOfItem,
|
|
parseFloat(this.amountInput),
|
|
this.unitCode,
|
|
this.descriptionOfCondition,
|
|
this.validThroughDateInput,
|
|
recipientDid,
|
|
projectId,
|
|
);
|
|
} else {
|
|
result = await createAndSubmitOffer(
|
|
this.axios,
|
|
this.apiServer,
|
|
this.activeDid,
|
|
this.descriptionOfItem,
|
|
parseFloat(this.amountInput),
|
|
this.unitCode,
|
|
this.descriptionOfCondition,
|
|
this.validThroughDateInput,
|
|
recipientDid,
|
|
projectId,
|
|
);
|
|
}
|
|
|
|
if (result.type === "error" || this.isCreationError(result.response)) {
|
|
const errorMessage = this.getCreationErrorMessage(result);
|
|
console.error("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.`,
|
|
},
|
|
5000,
|
|
);
|
|
localStorage.removeItem("imageUrl");
|
|
if (this.destinationPathAfter) {
|
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
|
} else {
|
|
(this.$router as Router).back();
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
console.error("Error with offer recordation caught:", error);
|
|
const errorMessage =
|
|
error.userMessage ||
|
|
error.response?.data?.error?.message ||
|
|
"There was an error recording the offer.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: errorMessage,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
|
|
constructOfferParam() {
|
|
const recipientDid = this.offeredToRecipient
|
|
? this.recipientDid
|
|
: undefined;
|
|
const projectId = this.offeredToProject ? this.projectId : undefined;
|
|
const offerClaim = hydrateOffer(
|
|
this.prevCredToEdit?.claim as OfferVerifiableCredential,
|
|
this.activeDid,
|
|
recipientDid,
|
|
this.descriptionOfItem,
|
|
parseFloat(this.amountInput),
|
|
this.unitCode,
|
|
this.descriptionOfCondition,
|
|
projectId,
|
|
this.validThroughDateInput,
|
|
this.prevCredToEdit?.id as string,
|
|
);
|
|
const claimStr = JSON.stringify(offerClaim);
|
|
return claimStr;
|
|
}
|
|
|
|
// 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
|
|
isCreationError(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
|
|
getCreationErrorMessage(result: any) {
|
|
return (
|
|
result.error?.userMessage ||
|
|
result.error?.error ||
|
|
result.response?.data?.error?.message
|
|
);
|
|
}
|
|
|
|
explainData() {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Data Sharing",
|
|
text: libsUtil.PRIVACY_MESSAGE,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
</script>
|
|
|