forked from trent_larson/crowd-funder-for-time-pwa
- Restore runMigrations functionality for database schema migrations - Remove indexedDBMigrationService.ts (was for IndexedDB to SQLite migration) - Recreate migrationService.ts and db-sql/migration.ts for schema management - Add proper TypeScript error handling with type guards in AccountViewView - Fix CreateAndSubmitClaimResult property access in QuickActionBvcBeginView - Remove LeafletMouseEvent from Vue components array (it's a type, not component) - Add null check for UserNameDialog callback to prevent undefined assignment - Implement extractErrorMessage helper function for consistent error handling - Update router to remove database-migration route The migration system now properly handles database schema evolution across app versions, while the IndexedDB to SQLite migration service has been removed as it was specific to that one-time migration.
372 lines
11 KiB
Vue
372 lines
11 KiB
Vue
<template>
|
|
<QuickNav selected="Contacts"></QuickNav>
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Breadcrumb -->
|
|
<div class="mb-8">
|
|
<h1
|
|
id="ViewBreadcrumb"
|
|
class="text-lg text-center font-light relative px-7"
|
|
>
|
|
<!-- Back -->
|
|
<router-link
|
|
:to="{ name: 'contacts' }"
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
|
</router-link>
|
|
</h1>
|
|
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
|
Transferred with {{ contact?.name }}
|
|
</h1>
|
|
</div>
|
|
|
|
<div class="flex justify-around">
|
|
<span />
|
|
<span class="justify-around">(Only 50 most recent)</span>
|
|
<span />
|
|
</div>
|
|
<div class="flex justify-around">
|
|
<span />
|
|
<span class="justify-around">
|
|
(This does not include claims by them if they're not visible to you.)
|
|
</span>
|
|
<span />
|
|
</div>
|
|
|
|
<!-- Results List -->
|
|
<table
|
|
class="table-auto w-full border-t border-slate-300 text-sm sm:text-base text-center"
|
|
>
|
|
<thead class="bg-slate-100">
|
|
<tr class="border-b border-slate-300">
|
|
<th></th>
|
|
<th class="px-1 py-2">From Them</th>
|
|
<th></th>
|
|
<th class="px-1 py-2">To Them</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="record in giveRecords"
|
|
:key="record.id"
|
|
class="border-b border-slate-300"
|
|
>
|
|
<td class="p-1 text-xs sm:text-sm text-left text-slate-500">
|
|
{{ new Date(record.issuedAt).toLocaleString() }}
|
|
</td>
|
|
<td class="p-1">
|
|
<span v-if="record.agentDid == contact?.did">
|
|
<div class="font-bold">
|
|
{{ displayAmount(record.unit, record.amount) }}
|
|
<span v-if="record.amountConfirmed" title="Confirmed">
|
|
<font-awesome
|
|
icon="circle-check"
|
|
class="text-green-600 fa-fw"
|
|
/>
|
|
</span>
|
|
<button v-else title="Unconfirmed" @click="confirm(record)">
|
|
<font-awesome icon="circle" class="text-blue-600 fa-fw" />
|
|
</button>
|
|
</div>
|
|
<div class="italic text-xs sm:text-sm text-slate-500">
|
|
{{ record.description }}
|
|
</div>
|
|
</span>
|
|
</td>
|
|
<td class="p-1">
|
|
<span v-if="record.agentDid == contact?.did">
|
|
<font-awesome icon="arrow-left" class="text-slate-400 fa-fw" />
|
|
</span>
|
|
<span v-else>
|
|
<font-awesome icon="arrow-right" class="text-slate-400 fa-fw" />
|
|
</span>
|
|
</td>
|
|
<td class="p-1">
|
|
<span v-if="record.agentDid != contact?.did">
|
|
<div class="font-bold">
|
|
{{ displayAmount(record.unit, record.amount) }}
|
|
<span v-if="record.amountConfirmed" title="Confirmed">
|
|
<font-awesome
|
|
icon="circle-check"
|
|
class="text-green-600 fa-fw"
|
|
/>
|
|
</span>
|
|
<button
|
|
v-else
|
|
title="Unconfirmed"
|
|
@click="cannotConfirmMessage()"
|
|
>
|
|
<font-awesome icon="circle" class="text-slate-600 fa-fw" />
|
|
</button>
|
|
</div>
|
|
<div class="italic text-xs sm:text-sm text-slate-500">
|
|
{{ record.description }}
|
|
</div>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { AxiosError, AxiosRequestHeaders } from "axios";
|
|
import * as R from "ramda";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import { NotificationIface } from "../constants/app";
|
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import * as databaseUtil from "../db/databaseUtil";
|
|
import {
|
|
AgreeVerifiableCredential,
|
|
GiveSummaryRecord,
|
|
GiveActionClaim,
|
|
} from "../interfaces";
|
|
import {
|
|
createEndorserJwtVcFromClaim,
|
|
displayAmount,
|
|
getHeaders,
|
|
SCHEMA_ORG_CONTEXT,
|
|
} from "../libs/endorserServer";
|
|
import { retrieveAccountCount } from "../libs/util";
|
|
import { logger } from "../utils/logger";
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
@Component({ components: { QuickNav } })
|
|
export default class ContactAmountssView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$route!: RouteLocationNormalizedLoaded;
|
|
$router!: Router;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
contact: Contact | null = null;
|
|
giveRecords: Array<GiveSummaryRecord> = [];
|
|
numAccounts = 0;
|
|
|
|
displayAmount = displayAmount;
|
|
|
|
async beforeCreate() {
|
|
this.numAccounts = await retrieveAccountCount();
|
|
}
|
|
|
|
async created() {
|
|
try {
|
|
const contactDid = this.$route.query["contactDid"] as string;
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const dbContact = await platformService.dbQuery(
|
|
"SELECT * FROM contacts WHERE did = ?",
|
|
[contactDid],
|
|
);
|
|
this.contact = databaseUtil.mapQueryResultToValues(
|
|
dbContact,
|
|
)[0] as unknown as Contact;
|
|
|
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
|
this.activeDid = settings?.activeDid || "";
|
|
this.apiServer = settings?.apiServer || "";
|
|
|
|
if (this.activeDid && this.contact) {
|
|
this.loadGives(this.activeDid, this.contact);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (err: any) {
|
|
logger.error("Error retrieving settings or gives.", err);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text:
|
|
err.userMessage ||
|
|
"There was an error retrieving your settings or contacts or gives.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadGives(activeDid: string, contact: Contact) {
|
|
try {
|
|
let result: Array<GiveSummaryRecord> = [];
|
|
const url =
|
|
this.apiServer +
|
|
"/api/v2/report/gives?agentDid=" +
|
|
encodeURIComponent(this.activeDid) +
|
|
"&recipientDid=" +
|
|
encodeURIComponent(contact.did);
|
|
const headers = await getHeaders(activeDid);
|
|
const resp = await this.axios.get(url, { headers });
|
|
if (resp.status === 200) {
|
|
result = resp.data.data;
|
|
} else {
|
|
logger.error(
|
|
"Got bad response status & data of",
|
|
resp.status,
|
|
resp.data,
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error With Server",
|
|
text: "Got an error retrieving your given time from the server.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
const url2 =
|
|
this.apiServer +
|
|
"/api/v2/report/gives?agentDid=" +
|
|
encodeURIComponent(contact.did) +
|
|
"&recipientDid=" +
|
|
encodeURIComponent(this.activeDid);
|
|
const headers2 = await getHeaders(activeDid);
|
|
const resp2 = await this.axios.get(url2, { headers: headers2 });
|
|
if (resp2.status === 200) {
|
|
result = R.concat(result, resp2.data.data);
|
|
} else {
|
|
logger.error(
|
|
"Got bad response status & data of",
|
|
resp2.status,
|
|
resp2.data,
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error With Server",
|
|
text: "Got an error retrieving your given time from the server.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
const sortedResult: Array<GiveSummaryRecord> = R.sort(
|
|
(a, b) =>
|
|
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
|
|
result,
|
|
);
|
|
this.giveRecords = sortedResult;
|
|
} catch (error) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error With Server",
|
|
text: error as string,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
async confirm(record: GiveSummaryRecord) {
|
|
// Make claim
|
|
// I use clone here because otherwise it gets a Proxy object.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const origClaim: GiveActionClaim = R.clone(record.fullClaim);
|
|
if (record.fullClaim["@context"] == SCHEMA_ORG_CONTEXT) {
|
|
delete origClaim["@context"];
|
|
}
|
|
origClaim["identifier"] = record.handleId;
|
|
const vcClaim: AgreeVerifiableCredential = {
|
|
"@context": SCHEMA_ORG_CONTEXT,
|
|
"@type": "AgreeAction",
|
|
object: origClaim,
|
|
};
|
|
|
|
const vcJwt: string = await createEndorserJwtVcFromClaim(
|
|
this.activeDid,
|
|
vcClaim,
|
|
);
|
|
|
|
// Make the xhr request payload
|
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
|
const url = this.apiServer + "/api/v2/claim";
|
|
const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
|
|
|
|
try {
|
|
const resp = await this.axios.post(url, payload, { headers });
|
|
if (resp.data?.success) {
|
|
record.amountConfirmed =
|
|
(origClaim.object?.amountOfThisGood as number) || 1;
|
|
}
|
|
} catch (error) {
|
|
let userMessage = "There was an error.";
|
|
const serverError = error as AxiosError;
|
|
if (serverError) {
|
|
if (serverError.message) {
|
|
userMessage = serverError.message; // Info for the user
|
|
} else {
|
|
userMessage = JSON.stringify(serverError.toJSON());
|
|
}
|
|
} else {
|
|
userMessage = error as string;
|
|
}
|
|
// Now set that error for the user to see.
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error With Server",
|
|
text: userMessage,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
cannotConfirmMessage() {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Not Allowed",
|
|
text: "Only the recipient can confirm final receipt.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
/*
|
|
Tooltip, generated on "title" attributes on "fa" icons
|
|
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
|
*/
|
|
/* Tooltip container */
|
|
.tooltip {
|
|
position: relative;
|
|
display: inline-block;
|
|
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
|
}
|
|
|
|
/* Tooltip text */
|
|
.tooltip .tooltiptext {
|
|
visibility: hidden;
|
|
width: 200px;
|
|
background-color: black;
|
|
color: #fff;
|
|
text-align: center;
|
|
padding: 5px 0;
|
|
border-radius: 6px;
|
|
|
|
position: absolute;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Show the tooltip text when you mouse over the tooltip container */
|
|
.tooltip:hover .tooltiptext {
|
|
visibility: visible;
|
|
}
|
|
.tooltip:hover .tooltiptext-left {
|
|
visibility: visible;
|
|
}
|
|
</style>
|