|
|
@ -43,6 +43,7 @@ |
|
|
|
|
|
|
|
<div class="flex justify-between" v-if="showGiveNumbers"> |
|
|
|
<div class="w-full text-right"> |
|
|
|
<!-- |
|
|
|
Hours to Add: |
|
|
|
<input |
|
|
|
class="border rounded border-slate-400 w-24 text-right" |
|
|
@ -57,30 +58,29 @@ |
|
|
|
placeholder="Description" |
|
|
|
v-model="hourDescriptionInput" |
|
|
|
/> |
|
|
|
<br /> |
|
|
|
--> |
|
|
|
In the following, only the most recent hours are included. To see more, |
|
|
|
click |
|
|
|
<span |
|
|
|
class="text-sm 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 py-1 rounded-md" |
|
|
|
> |
|
|
|
<fa icon="file-lines" class="fa-fw" /> |
|
|
|
</span> |
|
|
|
<br /> |
|
|
|
<button |
|
|
|
href="" |
|
|
|
class="text-center text-md text-white px-1.5 py-2 rounded-md mb-6" |
|
|
|
class="text-center text-md text-white px-1.5 py-2 rounded-md mt-1" |
|
|
|
v-bind:class="showGiveAmountsClassNames()" |
|
|
|
@click="toggleShowGiveTotals()" |
|
|
|
> |
|
|
|
{{ |
|
|
|
showGiveTotals |
|
|
|
? "Total" |
|
|
|
? "Showing Total" |
|
|
|
: showGiveConfirmed |
|
|
|
? "Confirmed" |
|
|
|
: "Unconfirmed" |
|
|
|
}} |
|
|
|
</button> |
|
|
|
<br /> |
|
|
|
(Only most recent hours included. To see more, click |
|
|
|
<span |
|
|
|
class="text-sm 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 py-1 rounded-md" |
|
|
|
> |
|
|
|
<fa icon="file-lines" class="fa-fw" /> |
|
|
|
</span> |
|
|
|
) |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
@ -189,10 +189,11 @@ |
|
|
|
> |
|
|
|
<button |
|
|
|
class="text-sm 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-1.5 rounded-l-md" |
|
|
|
@click="onClickAddGive(activeDid, contact.did)" |
|
|
|
@click="showGiftedDialog(activeDid, contact.did)" |
|
|
|
:title="givenByMeDescriptions[contact.did] || ''" |
|
|
|
> |
|
|
|
To: |
|
|
|
<br /> |
|
|
|
{{ |
|
|
|
/* eslint-disable prettier/prettier */ |
|
|
|
this.showGiveTotals |
|
|
@ -203,15 +204,17 @@ |
|
|
|
: (givenByMeUnconfirmed[contact.did] || 0) |
|
|
|
/* eslint-enable prettier/prettier */ |
|
|
|
}} |
|
|
|
<br /> |
|
|
|
<fa icon="plus" /> |
|
|
|
</button> |
|
|
|
|
|
|
|
<button |
|
|
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l" |
|
|
|
@click="onClickAddGive(contact.did, activeDid)" |
|
|
|
@click="showGiftedDialog(contact.did, this.activeDid)" |
|
|
|
:title="givenToMeDescriptions[contact.did] || ''" |
|
|
|
> |
|
|
|
From: |
|
|
|
<br /> |
|
|
|
{{ |
|
|
|
/* eslint-disable prettier/prettier */ |
|
|
|
this.showGiveTotals |
|
|
@ -222,6 +225,7 @@ |
|
|
|
: (givenToMeUnconfirmed[contact.did] || 0) |
|
|
|
/* eslint-enable prettier/prettier */ |
|
|
|
}} |
|
|
|
<br /> |
|
|
|
<fa icon="plus" /> |
|
|
|
</button> |
|
|
|
|
|
|
@ -237,7 +241,7 @@ |
|
|
|
name: 'contact-amounts', |
|
|
|
query: { contactDid: contact.did }, |
|
|
|
}" |
|
|
|
class="text-sm 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-1.5 rounded-md" |
|
|
|
class="text-sm 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-1.5 rounded-md border border-slate-400" |
|
|
|
title="See more given activity" |
|
|
|
> |
|
|
|
<fa icon="file-lines" class="fa-fw" /> |
|
|
@ -249,6 +253,7 @@ |
|
|
|
</ul> |
|
|
|
<p v-else>There are no contacts.</p> |
|
|
|
|
|
|
|
<GiftedDialog ref="customGivenDialog" /> |
|
|
|
<OfferDialog ref="customOfferDialog" /> |
|
|
|
|
|
|
|
<div v-if="showLargeIdenticon" class="fixed z-[100] top-0 inset-x-0 w-full"> |
|
|
@ -313,8 +318,8 @@ import { |
|
|
|
import { |
|
|
|
CONTACT_CSV_HEADER, |
|
|
|
CONTACT_URL_PREFIX, |
|
|
|
GiverReceiverInputInfo, |
|
|
|
GiveSummaryRecord, |
|
|
|
GiveVerifiableCredential, |
|
|
|
isDid, |
|
|
|
RegisterVerifiableCredential, |
|
|
|
SERVICE_ID, |
|
|
@ -322,13 +327,14 @@ import { |
|
|
|
import * as libsUtil from "@/libs/util"; |
|
|
|
import QuickNav from "@/components/QuickNav.vue"; |
|
|
|
import EntityIcon from "@/components/EntityIcon.vue"; |
|
|
|
import GiftedDialog from "@/components/GiftedDialog.vue"; |
|
|
|
import OfferDialog from "@/components/OfferDialog.vue"; |
|
|
|
import { Account } from "@/db/tables/accounts"; |
|
|
|
|
|
|
|
import { Buffer } from "buffer/"; |
|
|
|
|
|
|
|
@Component({ |
|
|
|
components: { EntityIcon, OfferDialog, QuickNav }, |
|
|
|
components: { GiftedDialog, EntityIcon, OfferDialog, QuickNav }, |
|
|
|
}) |
|
|
|
export default class ContactsView extends Vue { |
|
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void; |
|
|
@ -352,8 +358,6 @@ export default class ContactsView extends Vue { |
|
|
|
givenToMeConfirmed: Record<string, number> = {}; |
|
|
|
// { "did:...": amount } entry for each contact |
|
|
|
givenToMeUnconfirmed: Record<string, number> = {}; |
|
|
|
hourDescriptionInput = ""; |
|
|
|
hourInput = "0"; |
|
|
|
isRegistered = false; |
|
|
|
showDidCopy = false; |
|
|
|
showGiveNumbers = false; |
|
|
@ -1041,27 +1045,33 @@ export default class ContactsView extends Vue { |
|
|
|
} |
|
|
|
|
|
|
|
private nameForDid(contacts: Array<Contact>, did: string): string { |
|
|
|
if (did === this.activeDid) { |
|
|
|
return "you"; |
|
|
|
} |
|
|
|
const contact = R.find((con) => con.did == did, contacts); |
|
|
|
return this.nameForContact(contact); |
|
|
|
} |
|
|
|
|
|
|
|
private nameForContact(contact?: Contact, capitalize?: boolean): string { |
|
|
|
return contact?.name || (capitalize ? "T" : "t") + "his unnamed user"; |
|
|
|
return ( |
|
|
|
(contact?.name as string) || (capitalize ? "T" : "t") + "his unnamed user" |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
async onClickAddGive(fromDid: string, toDid: string): Promise<void> { |
|
|
|
const identity = await this.getIdentity(this.activeDid); |
|
|
|
|
|
|
|
private showGiftedDialog(giverDid: string, recipientDid: string) { |
|
|
|
// if they have unconfirmed amounts, ask to confirm those first |
|
|
|
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) { |
|
|
|
const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are"; |
|
|
|
const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours"; |
|
|
|
if ( |
|
|
|
recipientDid == this.activeDid && |
|
|
|
this.givenToMeUnconfirmed[giverDid] > 0 |
|
|
|
) { |
|
|
|
const isAre = this.givenToMeUnconfirmed[giverDid] == 1 ? "is" : "are"; |
|
|
|
const hours = this.givenToMeUnconfirmed[giverDid] == 1 ? "hour" : "hours"; |
|
|
|
if ( |
|
|
|
confirm( |
|
|
|
"There " + |
|
|
|
isare + |
|
|
|
isAre + |
|
|
|
" " + |
|
|
|
this.givenToMeUnconfirmed[fromDid] + |
|
|
|
this.givenToMeUnconfirmed[giverDid] + |
|
|
|
" unconfirmed " + |
|
|
|
hours + |
|
|
|
" from them." + |
|
|
@ -1070,178 +1080,58 @@ export default class ContactsView extends Vue { |
|
|
|
) { |
|
|
|
this.$router.push({ |
|
|
|
name: "contact-amounts", |
|
|
|
query: { contactDid: fromDid }, |
|
|
|
query: { contactDid: giverDid }, |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
if (!libsUtil.isNumeric(this.hourInput)) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Input Error", |
|
|
|
text: "This is not a valid number of hours: " + this.hourInput, |
|
|
|
}, |
|
|
|
3000, |
|
|
|
); |
|
|
|
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Input Error", |
|
|
|
text: "Giving no hours or description does nothing.", |
|
|
|
}, |
|
|
|
3000, |
|
|
|
); |
|
|
|
} else if (!identity) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Status Error", |
|
|
|
text: "No identifier is available.", |
|
|
|
}, |
|
|
|
3000, |
|
|
|
); |
|
|
|
|
|
|
|
let giver: GiverReceiverInputInfo, receiver: GiverReceiverInputInfo; |
|
|
|
if (giverDid) { |
|
|
|
giver = { |
|
|
|
did: giverDid, |
|
|
|
name: this.nameForDid(this.contacts, giverDid), |
|
|
|
}; |
|
|
|
} |
|
|
|
if (recipientDid) { |
|
|
|
receiver = { |
|
|
|
did: recipientDid, |
|
|
|
name: this.nameForDid(this.contacts, recipientDid), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
let callback: (amount: number) => void; |
|
|
|
let customTitle = "Given"; |
|
|
|
// choose whether to open dialog to user or from user |
|
|
|
if (giverDid == this.activeDid) { |
|
|
|
callback = (amount: number) => { |
|
|
|
const newList = R.clone(this.givenByMeUnconfirmed); |
|
|
|
newList[recipientDid] = (newList[recipientDid] || 0) + amount; |
|
|
|
this.givenByMeUnconfirmed = newList; |
|
|
|
}; |
|
|
|
customTitle = "To " + receiver.name; |
|
|
|
} else { |
|
|
|
// ask to confirm amount |
|
|
|
let toFrom; |
|
|
|
if (fromDid == identity?.did) { |
|
|
|
toFrom = "from you to " + this.nameForDid(this.contacts, toDid); |
|
|
|
} else { |
|
|
|
toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you"; |
|
|
|
} |
|
|
|
let description; |
|
|
|
if (this.hourDescriptionInput) { |
|
|
|
description = " with description '" + this.hourDescriptionInput + "'"; |
|
|
|
} else { |
|
|
|
description = " with no description"; |
|
|
|
} |
|
|
|
if ( |
|
|
|
confirm( |
|
|
|
"Are you sure you want to record " + |
|
|
|
this.hourInput + |
|
|
|
" hour" + |
|
|
|
(this.hourInput == "1" ? "" : "s") + |
|
|
|
" " + |
|
|
|
toFrom + |
|
|
|
description + |
|
|
|
"?", |
|
|
|
) |
|
|
|
) { |
|
|
|
this.createAndSubmitContactGive( |
|
|
|
identity, |
|
|
|
fromDid, |
|
|
|
toDid, |
|
|
|
parseFloat(this.hourInput), |
|
|
|
this.hourDescriptionInput, |
|
|
|
); |
|
|
|
} |
|
|
|
// must be (recipientDid == this.activeDid) |
|
|
|
callback = (amount: number) => { |
|
|
|
const newList = R.clone(this.givenToMeUnconfirmed); |
|
|
|
newList[giverDid] = (newList[giverDid] || 0) + amount; |
|
|
|
this.givenToMeUnconfirmed = newList; |
|
|
|
}; |
|
|
|
customTitle = "From " + giver.name; |
|
|
|
} |
|
|
|
(this.$refs.customGivenDialog as GiftedDialog).open( |
|
|
|
giver, |
|
|
|
receiver, |
|
|
|
undefined as string, |
|
|
|
customTitle, |
|
|
|
callback, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
openOfferDialog(recipientDid: string) { |
|
|
|
(this.$refs.customOfferDialog as OfferDialog).open(recipientDid); |
|
|
|
} |
|
|
|
|
|
|
|
// similar function is in endorserServer.ts |
|
|
|
private async createAndSubmitContactGive( |
|
|
|
identity: IIdentifier, |
|
|
|
fromDid: string, |
|
|
|
toDid: string, |
|
|
|
amount: number, |
|
|
|
description: string, |
|
|
|
): Promise<void> { |
|
|
|
// Make a claim |
|
|
|
const vcClaim: GiveVerifiableCredential = { |
|
|
|
"@context": "https://schema.org", |
|
|
|
"@type": "GiveAction", |
|
|
|
agent: { identifier: fromDid }, |
|
|
|
object: { amountOfThisGood: amount, unitCode: "HUR" }, |
|
|
|
recipient: { identifier: toDid }, |
|
|
|
}; |
|
|
|
if (description) { |
|
|
|
vcClaim.description = description; |
|
|
|
} |
|
|
|
// Make a payload for the claim |
|
|
|
const vcPayload = { |
|
|
|
vc: { |
|
|
|
"@context": ["https://www.w3.org/2018/credentials/v1"], |
|
|
|
type: ["VerifiableCredential"], |
|
|
|
credentialSubject: vcClaim, |
|
|
|
}, |
|
|
|
}; |
|
|
|
// Create a signature using private key of identity |
|
|
|
if (identity.keys[0].privateKeyHex !== null) { |
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
|
|
|
const privateKeyHex: string = identity.keys[0].privateKeyHex!; |
|
|
|
const signer = await SimpleSigner(privateKeyHex); |
|
|
|
const alg = undefined; |
|
|
|
// Create a JWT for the request |
|
|
|
const vcJwt: string = await didJwt.createJWT(vcPayload, { |
|
|
|
alg: alg, |
|
|
|
issuer: identity.did, |
|
|
|
signer: signer, |
|
|
|
}); |
|
|
|
|
|
|
|
// Make the xhr request payload |
|
|
|
|
|
|
|
const payload = JSON.stringify({ jwtEncoded: vcJwt }); |
|
|
|
const url = this.apiServer + "/api/v2/claim"; |
|
|
|
const headers = await this.getHeaders(identity); |
|
|
|
|
|
|
|
try { |
|
|
|
const resp = await this.axios.post(url, payload, { headers }); |
|
|
|
if (resp.data?.success?.handleId) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "success", |
|
|
|
title: "Done", |
|
|
|
text: "Successfully logged time to the server.", |
|
|
|
}, |
|
|
|
5000, |
|
|
|
); |
|
|
|
|
|
|
|
if (fromDid === identity.did) { |
|
|
|
const newList = R.clone(this.givenByMeUnconfirmed); |
|
|
|
newList[toDid] = (newList[toDid] || 0) + amount; |
|
|
|
this.givenByMeUnconfirmed = newList; |
|
|
|
} else { |
|
|
|
const newList = R.clone(this.givenToMeConfirmed); |
|
|
|
newList[fromDid] = (newList[fromDid] || 0) + amount; |
|
|
|
this.givenToMeConfirmed = newList; |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error("Error in createAndSubmitContactGive: ", error); |
|
|
|
let userMessage = "There was an error. See logs for more info."; |
|
|
|
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 Sending Give", |
|
|
|
text: userMessage, |
|
|
|
}, |
|
|
|
5000, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private async onClickCancelName() { |
|
|
|
this.contactEdit = null; |
|
|
|
this.contactNewName = ""; |
|
|
|