Trent Larson
8 months ago
38 changed files with 1185 additions and 349 deletions
@ -0,0 +1,4 @@ |
|||
# Only the variables that start with VUE_APP_ are seen in the application process.env in Vue. |
|||
|
|||
# this won't resolve as a URL on production; it's a URN only found in the test system |
|||
VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK |
@ -0,0 +1,4 @@ |
|||
# Only the variables that start with VUE_APP_ are seen in the application process.env in Vue. |
|||
VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H |
|||
VUE_APP_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch |
|||
VUE_APP_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app |
@ -0,0 +1,220 @@ |
|||
<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()" |
|||
> |
|||
<fa icon="chevron-left" class="fa-fw"></fa> |
|||
</h1> |
|||
</div> |
|||
|
|||
<!-- Heading --> |
|||
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4"> |
|||
Beginning of BVC Saturday Meeting |
|||
</h1> |
|||
|
|||
<div> |
|||
<h2 class="text-2xl m-2">You're Here</h2> |
|||
<div class="m-2 flex"> |
|||
<input type="checkbox" v-model="attended" class="h-6 w-6" /> |
|||
<span class="pb-2 pl-2 pr-2">Attended</span> |
|||
</div> |
|||
<div class="m-2 flex"> |
|||
<input type="checkbox" v-model="gaveTime" class="h-6 w-6" /> |
|||
<span class="pb-2 pl-2 pr-2">Spent Time</span> |
|||
<span v-if="gaveTime"> |
|||
<input |
|||
type="text" |
|||
placeholder="How much time" |
|||
v-model="hoursStr" |
|||
size="1" |
|||
class="border border-slate-400 h-6 px-2" |
|||
/> |
|||
hour(s) |
|||
</span> |
|||
<!-- This is to match input height to avoid shifting when hiding & showing. --> |
|||
<span v-else class="h-6" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
v-if="attended || (gaveTime && hoursStr && hoursStr != '0')" |
|||
class="flex justify-center mt-4" |
|||
> |
|||
<button |
|||
@click="record()" |
|||
class="block text-center text-md font-bold bg-blue-500 text-white px-2 py-3 rounded-md w-56" |
|||
> |
|||
Sign & Send |
|||
</button> |
|||
</div> |
|||
<div v-else class="flex justify-center mt-4"> |
|||
<button |
|||
class="block text-center text-md font-bold bg-slate-500 text-white px-2 py-3 rounded-md w-56" |
|||
> |
|||
Select Your Actions |
|||
</button> |
|||
</div> |
|||
</section> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import axios from "axios"; |
|||
import { DateTime } from "luxon"; |
|||
import { Component, Vue } from "vue-facing-decorator"; |
|||
|
|||
import QuickNav from "@/components/QuickNav.vue"; |
|||
import TopMessage from "@/components/TopMessage.vue"; |
|||
import { NotificationIface } from "@/constants/app"; |
|||
import { db } from "@/db/index"; |
|||
import { |
|||
BVC_MEETUPS_PROJECT_CLAIM_ID, |
|||
bvcMeetingJoinClaim, |
|||
createAndSubmitClaim, |
|||
createAndSubmitGive, |
|||
} from "@/libs/endorserServer"; |
|||
import * as libsUtil from "@/libs/util"; |
|||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; |
|||
|
|||
@Component({ |
|||
components: { |
|||
QuickNav, |
|||
TopMessage, |
|||
}, |
|||
}) |
|||
export default class QuickActionBvcBeginView extends Vue { |
|||
$notify!: (notification: NotificationIface, timeout?: number) => void; |
|||
|
|||
attended = true; |
|||
gaveTime = true; |
|||
hoursStr = "1"; |
|||
todayOrPreviousStartDate = ""; |
|||
|
|||
async mounted() { |
|||
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! |
|||
this.todayOrPreviousStartDate = |
|||
eventStartDateObj.toISO({ |
|||
suppressMilliseconds: true, |
|||
}) || ""; |
|||
} |
|||
|
|||
async record() { |
|||
await db.open(); |
|||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; |
|||
const activeDid = settings?.activeDid || ""; |
|||
const apiServer = settings?.apiServer || ""; |
|||
|
|||
try { |
|||
const hoursNum = libsUtil.numberOrZero(this.hoursStr); |
|||
const identity = await libsUtil.getIdentity(activeDid); |
|||
|
|||
// first send the claim for time given |
|||
let timeSuccess = false; |
|||
if (this.gaveTime && hoursNum > 0) { |
|||
const timeResult = await createAndSubmitGive( |
|||
axios, |
|||
apiServer, |
|||
identity, |
|||
activeDid, |
|||
undefined, |
|||
undefined, |
|||
hoursNum, |
|||
"HUR", |
|||
BVC_MEETUPS_PROJECT_CLAIM_ID, |
|||
); |
|||
if (timeResult.type === "success") { |
|||
timeSuccess = true; |
|||
} else { |
|||
console.error("Error sending time:", timeResult); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: |
|||
timeResult?.error?.userMessage || |
|||
"There was an error sending the time.", |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
} |
|||
|
|||
// now send the claim for attendance |
|||
let attendedSuccess = false; |
|||
if (this.attended) { |
|||
const attendResult = await createAndSubmitClaim( |
|||
bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate), |
|||
identity, |
|||
apiServer, |
|||
axios, |
|||
); |
|||
if (attendResult.type === "success") { |
|||
attendedSuccess = true; |
|||
} else { |
|||
console.error("Error sending attendance:", attendResult); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: |
|||
attendResult?.error?.userMessage || |
|||
"There was an error sending the attendance.", |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
} |
|||
|
|||
if (timeSuccess || attendedSuccess) { |
|||
const actions = |
|||
timeSuccess && attendedSuccess |
|||
? "Your attendance and time have been recorded." |
|||
: timeSuccess |
|||
? "Your time has been recorded." |
|||
: "Your attendance has been recorded."; |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "success", |
|||
title: "Success", |
|||
text: actions, |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|||
} catch (error: any) { |
|||
console.error("Error sending claims.", error); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: error.userMessage || "There was an error sending claims.", |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,377 @@ |
|||
<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()" |
|||
> |
|||
<fa icon="chevron-left" class="fa-fw"></fa> |
|||
</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"> |
|||
<fa icon="spinner" class="animate-spin" /> |
|||
</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 |
|||
class="border-b border-slate-300 py-2" |
|||
v-for="record in claimsToConfirm" |
|||
:key="record.id" |
|||
> |
|||
<div class="grid grid-cols-12"> |
|||
<span class="col-span-11 justify-self-start"> |
|||
<span> |
|||
<input |
|||
type="checkbox" |
|||
:checked="claimsToConfirmSelected.includes(record.id)" |
|||
@click=" |
|||
claimsToConfirmSelected.includes(record.id) |
|||
? claimsToConfirmSelected.splice( |
|||
claimsToConfirmSelected.indexOf(record.id), |
|||
1, |
|||
) |
|||
: claimsToConfirmSelected.push(record.id) |
|||
" |
|||
class="mr-2 h-6 w-6" |
|||
/> |
|||
</span> |
|||
{{ |
|||
claimSpecialDescription( |
|||
record, |
|||
activeDid, |
|||
allMyDids, |
|||
allContacts, |
|||
) |
|||
}} |
|||
<a @click="onClickLoadClaim(record.id)"> |
|||
<fa |
|||
icon="circle-info" |
|||
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 |
|||
<fa icon="users" class="text-slate-500" /> |
|||
page. |
|||
</span> |
|||
</div> |
|||
|
|||
<div> |
|||
<h2 class="text-2xl m-2">Anything else?</h2> |
|||
<div class="m-2 flex"> |
|||
<input type="checkbox" v-model="someoneGave" class="h-6 w-6" /> |
|||
<span class="pb-2 pl-2 pr-2">Someone else gave</span> |
|||
<span v-if="someoneGave"> |
|||
<input |
|||
type="text" |
|||
v-model="description" |
|||
size="20" |
|||
class="border border-slate-400 h-6 px-2" |
|||
/> |
|||
<br /> |
|||
(Everyone likes personalized messages! 😁) |
|||
</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 |
|||
@click="record()" |
|||
class="block text-center text-md font-bold bg-blue-500 text-white px-2 py-3 rounded-md w-56" |
|||
> |
|||
Sign & Send |
|||
</button> |
|||
</div> |
|||
<div v-else class="flex justify-center mt-4"> |
|||
<button |
|||
class="block text-center text-md font-bold bg-slate-500 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 { IIdentifier } from "@veramo/core"; |
|||
import { Component, Vue } from "vue-facing-decorator"; |
|||
|
|||
import QuickNav from "@/components/QuickNav.vue"; |
|||
import TopMessage from "@/components/TopMessage.vue"; |
|||
import { NotificationIface } from "@/constants/app"; |
|||
import { accountsDB, db } from "@/db/index"; |
|||
import { Account } from "@/db/tables/accounts"; |
|||
import { Contact } from "@/db/tables/contacts"; |
|||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; |
|||
import { accessToken } from "@/libs/crypto"; |
|||
import { |
|||
BVC_MEETUPS_PROJECT_CLAIM_ID, |
|||
claimSpecialDescription, |
|||
containsHiddenDid, |
|||
createAndSubmitConfirmation, |
|||
createAndSubmitGive, |
|||
ErrorResult, |
|||
GenericServerRecord, |
|||
GenericVerifiableCredential, |
|||
} from "@/libs/endorserServer"; |
|||
import * as libsUtil 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 = ""; |
|||
claimCountWithHidden = 0; |
|||
claimsToConfirm: GenericServerRecord[] = []; |
|||
claimsToConfirmSelected: string[] = []; |
|||
description = "breakfast"; |
|||
loadingConfirms = true; |
|||
someoneGave = false; |
|||
|
|||
async created() { |
|||
await db.open(); |
|||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; |
|||
this.apiServer = settings?.apiServer || ""; |
|||
this.activeDid = settings?.activeDid || ""; |
|||
this.allContacts = await db.contacts.toArray(); |
|||
} |
|||
|
|||
async mounted() { |
|||
this.loadingConfirms = true; |
|||
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, |
|||
}) || ""; |
|||
|
|||
await accountsDB.open(); |
|||
const allAccounts = await accountsDB.accounts.toArray(); |
|||
this.allMyDids = allAccounts.map((acc) => acc.did); |
|||
const account: Account | undefined = await accountsDB.accounts |
|||
.where("did") |
|||
.equals(this.activeDid) |
|||
.first(); |
|||
const identity: IIdentifier = JSON.parse( |
|||
(account?.identity as string) || "null", |
|||
); |
|||
const headers = { |
|||
Authorization: "Bearer " + (await accessToken(identity)), |
|||
}; |
|||
try { |
|||
const response = await fetch( |
|||
this.apiServer + |
|||
"/api/claim/?" + |
|||
"issuedAt_greaterThanOrEqualTo=" + |
|||
encodeURIComponent(todayOrPreviousStartDate) + |
|||
"&excludeConfirmations=true", |
|||
{ headers }, |
|||
); |
|||
|
|||
if (!response.ok) { |
|||
console.log("Bad response", response); |
|||
throw new Error("Bad response when retrieving claims."); |
|||
} |
|||
await response.json().then((data) => { |
|||
const dataByOthers = R.reject( |
|||
(claim: GenericServerRecord) => claim.issuer === this.activeDid, |
|||
data, |
|||
); |
|||
const dataByOthersWithoutHidden = R.reject( |
|||
containsHiddenDid, |
|||
dataByOthers, |
|||
); |
|||
this.claimsToConfirm = dataByOthersWithoutHidden; |
|||
this.claimCountWithHidden = |
|||
dataByOthers.length - dataByOthersWithoutHidden.length; |
|||
}); |
|||
} catch (error) { |
|||
console.error("Error:", error); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: "There was an error retrieving today's claims to confirm.", |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
this.loadingConfirms = false; |
|||
} |
|||
|
|||
onClickLoadClaim(jwtId: string) { |
|||
const route = { |
|||
path: "/claim/" + encodeURIComponent(jwtId), |
|||
}; |
|||
this.$router.push(route); |
|||
} |
|||
|
|||
async record() { |
|||
try { |
|||
const identity = await libsUtil.getIdentity(this.activeDid); |
|||
|
|||
// in parallel, make a confirmation for each selected claim and send them all to the server |
|||
const confirmResults = await Promise.allSettled( |
|||
this.claimsToConfirmSelected.map(async (jwtId) => { |
|||
const record = this.claimsToConfirm.find( |
|||
(claim) => claim.id === jwtId, |
|||
); |
|||
if (!record) { |
|||
return { type: "error", error: "Record not found." }; |
|||
} |
|||
const identity = await libsUtil.getIdentity(this.activeDid); |
|||
return createAndSubmitConfirmation( |
|||
identity, |
|||
record.claim as GenericVerifiableCredential, |
|||
record.id, |
|||
record.handleId, |
|||
this.apiServer, |
|||
axios, |
|||
); |
|||
}), |
|||
); |
|||
// check for any rejected confirmations |
|||
const confirmsSucceeded = confirmResults.filter( |
|||
(result) => |
|||
result.status === "fulfilled" && result.value.type === "success", |
|||
); |
|||
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) { |
|||
console.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.`, |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
|
|||
// now send the give for the description |
|||
let giveSucceeded = false; |
|||
if (this.someoneGave) { |
|||
const giveResult = await createAndSubmitGive( |
|||
axios, |
|||
this.apiServer, |
|||
identity, |
|||
undefined, |
|||
this.activeDid, |
|||
this.description, |
|||
undefined, |
|||
undefined, |
|||
BVC_MEETUPS_PROJECT_CLAIM_ID, |
|||
); |
|||
giveSucceeded = giveResult.type === "success"; |
|||
if (!giveSucceeded) { |
|||
console.error("Error sending give:", giveResult); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: |
|||
(giveResult as ErrorResult)?.error?.userMessage || |
|||
"There was an error sending that give.", |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
} |
|||
|
|||
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, |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|||
} catch (error: any) { |
|||
console.error("Error sending claims.", error); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: error.userMessage || "There was an error sending claims.", |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,52 @@ |
|||
<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()" |
|||
> |
|||
<fa icon="chevron-left" class="fa-fw"></fa> |
|||
</h1> |
|||
</div> |
|||
|
|||
<!-- Heading --> |
|||
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4"> |
|||
Bountiful Voluntaryist Community Actions |
|||
</h1> |
|||
|
|||
<div> |
|||
<router-link |
|||
:to="{ name: 'quick-action-bvc-begin' }" |
|||
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md" |
|||
> |
|||
Beginning of Meeting |
|||
</router-link> |
|||
<router-link |
|||
:to="{ name: 'quick-action-bvc-end' }" |
|||
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md" |
|||
> |
|||
End of Meeting |
|||
</router-link> |
|||
</div> |
|||
</section> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Vue } from "vue-facing-decorator"; |
|||
|
|||
import QuickNav from "@/components/QuickNav.vue"; |
|||
import TopMessage from "@/components/TopMessage.vue"; |
|||
|
|||
@Component({ |
|||
components: { |
|||
QuickNav, |
|||
TopMessage, |
|||
}, |
|||
}) |
|||
export default class QuickActionBvcView extends Vue {} |
|||
</script> |
Loading…
Reference in new issue