forked from jsnbuchanan/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.
432 lines
14 KiB
Vue
432 lines
14 KiB
Vue
<template>
|
|
<QuickNav />
|
|
<TopMessage />
|
|
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Back -->
|
|
<div 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="$router.back()"
|
|
>
|
|
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Heading -->
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4">
|
|
End of BVC Saturday Meeting
|
|
</h1>
|
|
|
|
<div>
|
|
<h2 class="text-2xl m-2">Confirm</h2>
|
|
<div v-if="loadingConfirms" class="flex justify-center">
|
|
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
|
</div>
|
|
<div v-else-if="claimsToConfirm.length === 0">
|
|
There are no claims yet today for you to confirm.
|
|
</div>
|
|
<ul class="border-t border-slate-300 m-2">
|
|
<li
|
|
v-for="record in claimsToConfirm"
|
|
:key="record.id"
|
|
class="border-b border-slate-300 py-2"
|
|
>
|
|
<div class="grid grid-cols-12">
|
|
<span class="col-span-11 justify-self-start">
|
|
<span>
|
|
<input
|
|
type="checkbox"
|
|
:checked="claimsToConfirmSelected.includes(record.id)"
|
|
class="mr-2 h-6 w-6"
|
|
@click="
|
|
claimsToConfirmSelected.includes(record.id)
|
|
? claimsToConfirmSelected.splice(
|
|
claimsToConfirmSelected.indexOf(record.id),
|
|
1,
|
|
)
|
|
: claimsToConfirmSelected.push(record.id)
|
|
"
|
|
/>
|
|
</span>
|
|
{{
|
|
claimSpecialDescription(
|
|
record,
|
|
activeDid,
|
|
allMyDids,
|
|
allContacts,
|
|
)
|
|
}}
|
|
<a @click="onClickLoadClaim(record.id)">
|
|
<font-awesome
|
|
icon="file-lines"
|
|
class="pl-2 text-blue-500 cursor-pointer"
|
|
/>
|
|
</a>
|
|
</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
|
|
<span>
|
|
{{
|
|
claimCountWithHidden === 1
|
|
? "There is 1 other claim with hidden details,"
|
|
: `There are ${claimCountWithHidden} other claims with hidden details,`
|
|
}}
|
|
so if you expected but do not see details from someone then ask them to
|
|
check that their activity is visible to you on their Contacts
|
|
<font-awesome icon="users" class="text-slate-500" />
|
|
page.
|
|
</span>
|
|
</div>
|
|
<div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
|
|
<span>
|
|
{{
|
|
claimCountByUser === 1
|
|
? "There is 1 other claim by you"
|
|
: `There are ${claimCountByUser} other claims by you`
|
|
}}
|
|
which you don't need to confirm.
|
|
</span>
|
|
</div>
|
|
|
|
<div>
|
|
<h2 class="text-2xl m-2">Anything else?</h2>
|
|
<div class="m-2 flex">
|
|
<input v-model="someoneGave" type="checkbox" class="h-6 w-6" />
|
|
<span class="pb-2 pl-2 pr-2">The group provided</span>
|
|
<span v-if="someoneGave">
|
|
<input
|
|
v-model="description"
|
|
type="text"
|
|
size="20"
|
|
class="border border-slate-400 h-6 px-2"
|
|
/>
|
|
<br />
|
|
(Everyone likes personalized messages! 😁 ... and for a pic:
|
|
<input v-model="supplyGiftDetails" type="checkbox" />)
|
|
</span>
|
|
<!-- This is to match input height to avoid shifting when hiding & showing. -->
|
|
<span v-else class="h-6">...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="claimsToConfirmSelected.length || (someoneGave && description)"
|
|
class="flex justify-center mt-4"
|
|
>
|
|
<button
|
|
class="block text-center text-md font-bold 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 w-56"
|
|
@click="record()"
|
|
>
|
|
Sign & Send
|
|
</button>
|
|
</div>
|
|
<div v-else class="flex justify-center mt-4">
|
|
<button
|
|
class="block text-center text-md font-bold 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-2 py-3 rounded-md w-56"
|
|
>
|
|
Choose What To Confirm
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import axios from "axios";
|
|
import { DateTime } from "luxon";
|
|
import * as R from "ramda";
|
|
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 * as databaseUtil from "../db/databaseUtil";
|
|
import {
|
|
accountsDBPromise,
|
|
db,
|
|
retrieveSettingsForActiveAccount,
|
|
} from "../db/index";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import {
|
|
GenericCredWrapper,
|
|
GenericVerifiableCredential,
|
|
CreateAndSubmitClaimResult,
|
|
} from "../interfaces";
|
|
import {
|
|
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
|
claimSpecialDescription,
|
|
containsHiddenDid,
|
|
createAndSubmitConfirmation,
|
|
createAndSubmitGive,
|
|
getHeaders,
|
|
} from "../libs/endorserServer";
|
|
import { logger } from "../utils/logger";
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
import { retrieveAllAccountsMetadata } from "@/libs/util";
|
|
@Component({
|
|
methods: { claimSpecialDescription },
|
|
components: {
|
|
QuickNav,
|
|
TopMessage,
|
|
},
|
|
})
|
|
export default class QuickActionBvcBeginView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
activeDid = "";
|
|
allContacts: Array<Contact> = [];
|
|
allMyDids: Array<string> = [];
|
|
apiServer = "";
|
|
claimCountByUser = 0;
|
|
claimCountWithHidden = 0;
|
|
claimsToConfirm: GenericCredWrapper<GenericVerifiableCredential>[] = [];
|
|
claimsToConfirmSelected: string[] = [];
|
|
description = "breakfast";
|
|
loadingConfirms = true;
|
|
someoneGave = false;
|
|
supplyGiftDetails = false;
|
|
|
|
async created() {
|
|
this.loadingConfirms = true;
|
|
|
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
|
this.apiServer = settings.apiServer || "";
|
|
this.activeDid = settings.activeDid || "";
|
|
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const contactQueryResult = await platformService.dbQuery(
|
|
"SELECT * FROM contacts",
|
|
);
|
|
this.allContacts = databaseUtil.mapQueryResultToValues(
|
|
contactQueryResult,
|
|
) as unknown as Contact[];
|
|
|
|
let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
|
|
if (currentOrPreviousSat.weekday < 6) {
|
|
// it's not Saturday or Sunday,
|
|
// so move back one week before setting to the Saturday
|
|
currentOrPreviousSat = currentOrPreviousSat.minus({ week: 1 });
|
|
}
|
|
const eventStartDateObj = currentOrPreviousSat
|
|
.set({ weekday: 6 })
|
|
.set({ hour: 9 })
|
|
.startOf("hour");
|
|
|
|
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
|
|
const todayOrPreviousStartDate =
|
|
eventStartDateObj.toISO({
|
|
suppressMilliseconds: true,
|
|
}) || "";
|
|
|
|
this.allMyDids = (await retrieveAllAccountsMetadata()).map(
|
|
(account) => account.did,
|
|
);
|
|
const headers = await getHeaders(this.activeDid);
|
|
try {
|
|
const response = await fetch(
|
|
this.apiServer +
|
|
"/api/claim/?" +
|
|
"issuedAt_greaterThanOrEqualTo=" +
|
|
encodeURIComponent(todayOrPreviousStartDate) +
|
|
"&excludeConfirmations=true",
|
|
{ headers },
|
|
);
|
|
|
|
if (!response.ok) {
|
|
logger.error("Bad response", response);
|
|
throw new Error("Bad response when retrieving claims.");
|
|
}
|
|
await response.json().then((data) => {
|
|
const dataByOthers = R.reject(
|
|
(claim: GenericCredWrapper<GenericVerifiableCredential>) =>
|
|
claim.issuer === this.activeDid,
|
|
data,
|
|
);
|
|
const dataByOthersWithoutHidden = R.reject(
|
|
containsHiddenDid,
|
|
dataByOthers,
|
|
);
|
|
this.claimsToConfirm = dataByOthersWithoutHidden;
|
|
this.claimCountByUser = data.length - dataByOthers.length;
|
|
this.claimCountWithHidden =
|
|
dataByOthers.length - dataByOthersWithoutHidden.length;
|
|
});
|
|
} catch (error) {
|
|
logger.error("Error:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "There was an error retrieving today's claims to confirm.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
this.loadingConfirms = false;
|
|
}
|
|
|
|
onClickLoadClaim(jwtId: string) {
|
|
const route = {
|
|
path: "/claim/" + encodeURIComponent(jwtId),
|
|
};
|
|
(this.$router as Router).push(route);
|
|
}
|
|
|
|
async record() {
|
|
try {
|
|
if (this.claimsToConfirmSelected.length > 0) {
|
|
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
|
|
}
|
|
|
|
// in parallel, make a confirmation for each selected claim and send them all to the server
|
|
const confirmResults: PromiseSettledResult<CreateAndSubmitClaimResult>[] =
|
|
await Promise.allSettled(
|
|
this.claimsToConfirmSelected.map(async (jwtId) => {
|
|
const record = this.claimsToConfirm.find(
|
|
(claim) => claim.id === jwtId,
|
|
);
|
|
if (!record) {
|
|
return { success: false, error: "Record not found." };
|
|
}
|
|
return createAndSubmitConfirmation(
|
|
this.activeDid,
|
|
record.claim as GenericVerifiableCredential,
|
|
record.id,
|
|
record.handleId,
|
|
this.apiServer,
|
|
axios,
|
|
);
|
|
}),
|
|
);
|
|
// check for any rejected confirmations
|
|
const confirmsSucceeded = confirmResults.filter(
|
|
// 'fulfilled' is the status in a successful PromiseFulfilledResult
|
|
(result) => result.status === "fulfilled" && result.value.success,
|
|
);
|
|
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
|
|
logger.error("Error sending confirmations:", confirmResults);
|
|
const howMany = confirmsSucceeded.length === 0 ? "all" : "some";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: `There was an error sending ${howMany} of the confirmations.`,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
// now send the give for the description
|
|
let giveSucceeded = false;
|
|
if (this.someoneGave && !this.supplyGiftDetails) {
|
|
const giveResult = await createAndSubmitGive(
|
|
axios,
|
|
this.apiServer,
|
|
this.activeDid,
|
|
undefined,
|
|
this.activeDid,
|
|
this.description,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
undefined,
|
|
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
|
);
|
|
giveSucceeded = giveResult.success;
|
|
if (!giveSucceeded) {
|
|
logger.error("Error sending give:", giveResult);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text:
|
|
(giveResult as CreateAndSubmitClaimResult)?.error ||
|
|
"There was an error sending that give.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
if (this.someoneGave && this.supplyGiftDetails) {
|
|
// we'll give a success message for the confirmations and go to the gifted details page
|
|
if (confirmsSucceeded.length > 0) {
|
|
const actions =
|
|
confirmsSucceeded.length === 1
|
|
? `Your confirmation has been recorded.`
|
|
: `Your confirmations have been recorded.`;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Success",
|
|
text: actions,
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
(this.$router as Router).push({
|
|
name: "gifted-details",
|
|
query: {
|
|
description: this.description,
|
|
destinationPathAfter: "/",
|
|
providerProjectId: BVC_MEETUPS_PROJECT_CLAIM_ID,
|
|
recipientDid: this.activeDid,
|
|
},
|
|
});
|
|
} else {
|
|
// just go ahead and print a message for all the activity
|
|
if (confirmsSucceeded.length > 0 || giveSucceeded) {
|
|
const confirms =
|
|
confirmsSucceeded.length === 1 ? "confirmation" : "confirmations";
|
|
const actions =
|
|
confirmsSucceeded.length > 0 && giveSucceeded
|
|
? `Your ${confirms} and that give have been recorded.`
|
|
: giveSucceeded
|
|
? "That give has been recorded."
|
|
: "Your " +
|
|
confirms +
|
|
" " +
|
|
(confirmsSucceeded.length === 1 ? "has" : "have") +
|
|
" been recorded.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Success",
|
|
text: actions,
|
|
},
|
|
3000,
|
|
);
|
|
(this.$router as Router).push({ path: "/" });
|
|
} else {
|
|
// errors should have already shown
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
logger.error("Error sending claims.", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: error.userMessage || "There was an error sending claims.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
</script>
|