<template> <QuickNav selected="Projects"></QuickNav> <!-- CONTENT --> <section id="Content" class="p-6 pb-24"> <!-- Breadcrumb --> <div id="ViewBreadcrumb" class="mb-8"> <h1 class="text-lg text-center font-light relative px-7"> <!-- Back --> <button @click="$router.go(-1)" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" > <fa icon="chevron-left" class="fa-fw"></fa> </button> <!-- Context Menu --> <a href="" class="text-lg text-center px-2 py-1 absolute -right-2 -top-1" ><fa icon="ellipsis-vertical" class="fa-fw"></fa ></a> View Plan </h1> </div> <div class="text-red-500"> {{ errorMessage }} </div> <!-- Project Details --> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div> <h2 class="text-xl font-semibold">{{ name }}</h2> <div class="flex justify-between gap-4 text-sm mb-3"> <span><fa icon="user" class="fa-fw text-slate-400"></fa> Rotary</span> <span ><fa icon="calendar" class="fa-fw text-slate-400"></fa >{{ timeSince }} </span> </div> <div class="text-sm text-slate-500"> <div v-if="!expanded"> {{ truncatedDesc }} <a v-if="description.length >= truncateLength" @click="expandText" >Read More</a > </div> <div v-else> {{ description }} <a @click="collapseText" class="uppercase text-xs font-semibold text-slate-700" >Read Less</a > </div> </div> </div> <button type="button" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" @click="onEditClick()" > Edit </button> </div> <div> <div v-if="activeDid"> <button @click="openDialog({ name: 'you', did: activeDid })" class="text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md" > I gave... </button> ― or: </div> <!-- similar contact selection code is in multiple places --> Record a gift from <span v-for="contact in allContacts" :key="contact.did"> <button @click="openDialog(contact)" class="text-blue-500"> {{ contact.name }}</button >, </span> <span v-if="allContacts.length > 0"> or </span> <button @click="openDialog()" class="text-blue-500"> someone not specified </button> </div> <!-- Gifts to & from this --> <div class="mt-8 flex justify-around"> <div> <h1 class="text-xl">Given to this Project</h1> </div> <div> <h1 class="text-xl">... and from this Project</h1> </div> </div> <div class="flex justify-around"> <div class="w-1/2"> <div v-for="give in givesToThis" :key="give.id"> <div class="flex justify-between"> <div class="flex gap-3"> <div class="flex gap-2"> <fa icon="user" class="fa-fw text-slate-400"></fa> <span>{{ didInfo(give.agentDid, activeDid, accounts, allContacts) }}</span> </div> <div class="flex gap-2" v-if="give.amount"> <fa icon="coins" class="fa-fw text-slate-400"></fa> <span>{{ give.amount }}</span> </div> <div class="flex gap-2" v-if="give.description"> <fa icon="comment" class="fa-fw text-slate-400"></fa> <span>{{ give.description }}</span> </div> </div> </div> </div> </div> <div class="w-1/2"> <div v-for="give in givesByThis" :key="give.id"> <div class="flex justify-between"> <div class="flex gap-3"> <div class="flex gap-2"> <fa icon="user" class="fa-fw text-slate-400"></fa> <span>{{ didInfo(give.agentDid, activeDid, accounts, allContacts) }}</span> </div> <div class="flex gap-2" v-if="give.amount"> <fa icon="coins" class="fa-fw text-slate-400"></fa> <span>{{ give.amount }}</span> </div> <div class="flex gap-2"> <fa icon="comment" class="fa-fw text-slate-400"></fa> <span>{{ give.description }}</span> </div> </div> </div> </div> </div> </div> <GiftedDialog ref="customDialog" @dialog-result="handleDialogResult" message="Received from" > </GiftedDialog> <AlertMessage :alertTitle="alertTitle" :alertMessage="alertMessage" ></AlertMessage> </section> </template> <script lang="ts"> import { AxiosError } from "axios"; import * as moment from "moment"; import { IIdentifier } from "@veramo/core"; import { Component, Vue } from "vue-facing-decorator"; import GiftedDialog from "@/components/GiftedDialog.vue"; import { accountsDB, db } from "@/db"; import { AccountsSchema } from "@/db/tables/accounts"; import { Contact } from "@/db/tables/contacts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { accessToken } from "@/libs/crypto"; import { createAndSubmitGive, didInfo, GiveServerRecord, } from "@/libs/endorserServer"; import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav"; @Component({ components: { GiftedDialog, AlertMessage, QuickNav }, }) export default class ProjectViewView extends Vue { accounts: AccountsSchema; activeDid = ""; alertMessage = ""; alertTitle = ""; allContacts: Array<Contact> = []; apiServer = ""; description = ""; errorMessage = ""; expanded = false; givesToThis: Array<GiveServerRecord> = []; givesByThis: Array<GiveServerRecord> = []; name = ""; numAccounts = 0; projectId = localStorage.getItem("projectId") || ""; // handle ID timeSince = ""; truncatedDesc = ""; truncateLength = 40; async beforeCreate() { accountsDB.open(); this.accounts = accountsDB.accounts; this.numAccounts = (await this.accounts?.count()) || 0; } async created() { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = settings?.activeDid || ""; this.apiServer = settings?.apiServer || ""; this.allContacts = await db.contacts.toArray(); this.accounts = accountsDB.accounts; const accountsArr = await this.accounts?.toArray(); const account = accountsArr.find((acc) => acc.did === this.activeDid); const identity = JSON.parse(account?.identity || "null"); this.LoadProject(identity); } public async getIdentity(activeDid) { await accountsDB.open(); const account = await accountsDB.accounts .where("did") .equals(activeDid) .first(); const identity = JSON.parse(account?.identity || "null"); if (!identity) { throw new Error( "Attempted to load project records with no identity available.", ); } return identity; } public async getHeaders(identity) { const token = await accessToken(identity); const headers = { "Content-Type": "application/json", Authorization: "Bearer " + token, }; return headers; } onEditClick() { localStorage.setItem("projectId", this.projectId as string); const route = { name: "new-edit-project", }; this.$router.push(route); } // Isn't there a better way to make this available to the template? didInfo(did, activeDid, identities, contacts) { return didInfo(did, activeDid, identities, contacts); } expandText() { this.expanded = true; } collapseText() { this.expanded = false; } async LoadProject(identity: IIdentifier) { const url = this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(this.projectId); const headers = { "Content-Type": "application/json", }; if (identity) { const token = await accessToken(identity); headers["Authorization"] = "Bearer " + token; } try { const resp = await this.axios.get(url, { headers }); if (resp.status === 200) { // feel free to remove this; I haven't yet because it's helpful console.log('Loaded project: ', resp.data); const startTime = resp.data.startTime; if (startTime != null) { const eventDate = new Date(startTime); const now = moment.now(); this.timeSince = moment.utc(now).to(eventDate); } this.name = resp.data.claim?.name || "(no name)"; this.description = resp.data.claim?.description || "(no description)"; this.truncatedDesc = this.description.slice(0, this.truncateLength); } else if (resp.status === 404) { // actually, axios throws an error so we never get here this.errorMessage = "That project does not exist."; } } catch (error: unknown) { const serverError = error as AxiosError; if (serverError.response?.status === 404) { this.errorMessage = "That project does not exist."; } else { this.errorMessage = "Something went wrong retrieving that project." + " See logs for more info."; console.error("Error retrieving project:", error); } } const givesInUrl = this.apiServer + "/api/v2/report/givesForPlans?planIds=" + encodeURIComponent(JSON.stringify([this.projectId])); try { const resp = await this.axios.get(givesInUrl, { headers }); if (resp.status === 200 && resp.data.data) { this.givesToThis = resp.data.data; } else { this.errorMessage = "Failed to retrieve gives to this project."; } } catch (error: unknown) { console.error("Error retrieving gives to this project:", error); const serverError = error as AxiosError; this.errorMessage = "Something went wrong retrieving gives to this project."; } const givesOutUrl = this.apiServer + "/api/v2/report/givesProvidedBy?providerId=" + encodeURIComponent(this.projectId); try { const resp = await this.axios.get(givesOutUrl, { headers }); if (resp.status === 200 && resp.data.data) { this.givesByThis = resp.data.data; } else { this.errorMessage = "Failed to retrieve gives by this project."; } } catch (error: unknown) { console.error("Error retrieving gives by this project:", error); const serverError = error as AxiosError; this.errorMessage = "Something went wrong retrieving gives by project."; } } openDialog(contact) { this.$refs.customDialog.open(contact); } handleDialogResult(result) { if (result.action === "confirm") { return new Promise((resolve) => { this.recordGive(result.contact?.did, result.description, result.hours); resolve(); }); } else { // action was not "confirm" so do nothing } } /** * * @param giverDid may be null * @param description may be an empty string * @param hours may be 0 */ async recordGive(giverDid, description, hours) { if (!this.activeDid) { this.alertTitle = "Error"; this.alertMessage = "You must select an identity before you can record a give."; return; } if (!description && !hours) { this.alertTitle = "Error"; this.alertMessage = "You must enter a description or some number of hours."; return; } try { const identity = await this.getIdentity(this.activeDid); const result = await createAndSubmitGive( this.axios, this.apiServer, identity, giverDid, this.activeDid, description, hours, this.projectId, ); if (result.status !== 201 || result.data?.error) { console.log("Error with give result:", result); this.alertTitle = "Error"; this.alertMessage = result.data?.error?.message || "There was an error recording the give."; } else { this.alertTitle = "Success"; this.alertMessage = "That gift was recorded."; } } catch (e) { console.log("Error with give caught:", e); this.alertTitle = "Error"; this.alertMessage = e.userMessage || e.response?.data?.error?.message || "There was an error recording the give."; } } } </script>