You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
740 lines
19 KiB
740 lines
19 KiB
<template>
|
|
<div v-if="visible" class="dialog-overlay">
|
|
<div class="dialog">
|
|
<!-- Step 1: Entity Selection -->
|
|
<EntitySelectionStep
|
|
v-show="firstStep"
|
|
:step-type="stepType"
|
|
:giver-entity-type="giverEntityType"
|
|
:recipient-entity-type="recipientEntityType"
|
|
:show-projects="showProjects"
|
|
:is-from-project-view="isFromProjectView"
|
|
:projects="projects"
|
|
:all-contacts="allContacts"
|
|
:active-did="activeDid"
|
|
:all-my-dids="allMyDids"
|
|
:conflict-checker="wouldCreateConflict"
|
|
:from-project-id="fromProjectId"
|
|
:to-project-id="toProjectId"
|
|
:giver="giver"
|
|
:receiver="receiver"
|
|
@entity-selected="handleEntitySelected"
|
|
@cancel="cancel"
|
|
/>
|
|
|
|
<!-- Step 2: Gift Details -->
|
|
<GiftDetailsStep
|
|
v-show="!firstStep"
|
|
:giver="giver"
|
|
:receiver="receiver"
|
|
:giver-entity-type="giverEntityType"
|
|
:recipient-entity-type="recipientEntityType"
|
|
:description="description"
|
|
:amount="parseFloat(amountInput) || 0"
|
|
:unit-code="unitCode"
|
|
:prompt="prompt"
|
|
:is-from-project-view="isFromProjectView"
|
|
:has-conflict="hasPersonConflict"
|
|
:offer-id="offerId"
|
|
:from-project-id="fromProjectId"
|
|
:to-project-id="toProjectId"
|
|
@update:description="description = $event"
|
|
@update:amount="handleAmountUpdate"
|
|
@update:unit-code="unitCode = $event"
|
|
@edit-entity="handleEditEntity"
|
|
@explain-data="explainData"
|
|
@submit="handleSubmit"
|
|
@cancel="cancel"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
|
|
|
|
import { NotificationIface } from "../constants/app";
|
|
import {
|
|
createAndSubmitGive,
|
|
didInfo,
|
|
serverMessageForUser,
|
|
getHeaders,
|
|
} from "../libs/endorserServer";
|
|
import * as libsUtil from "../libs/util";
|
|
// Removed unused imports: db, retrieveSettingsForActiveAccount
|
|
import { Contact } from "../db/tables/contacts";
|
|
import * as databaseUtil from "../db/databaseUtil";
|
|
import { retrieveAccountDids } from "../libs/util";
|
|
import { logger } from "../utils/logger";
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
import EntityIcon from "../components/EntityIcon.vue";
|
|
import ProjectIcon from "../components/ProjectIcon.vue";
|
|
import EntitySelectionStep from "../components/EntitySelectionStep.vue";
|
|
import GiftDetailsStep from "../components/GiftDetailsStep.vue";
|
|
import { PlanData } from "../interfaces/records";
|
|
|
|
@Component({
|
|
components: {
|
|
EntityIcon,
|
|
ProjectIcon,
|
|
EntitySelectionStep,
|
|
GiftDetailsStep,
|
|
},
|
|
})
|
|
export default class GiftedDialog extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
@Prop() fromProjectId = "";
|
|
@Prop() toProjectId = "";
|
|
@Prop({ default: false }) showProjects = false;
|
|
@Prop() isFromProjectView = false;
|
|
|
|
@Watch("showProjects")
|
|
onShowProjectsChange() {
|
|
this.updateEntityTypes();
|
|
}
|
|
|
|
@Watch("fromProjectId")
|
|
onFromProjectIdChange() {
|
|
this.updateEntityTypes();
|
|
}
|
|
|
|
@Watch("toProjectId")
|
|
onToProjectIdChange() {
|
|
this.updateEntityTypes();
|
|
}
|
|
|
|
activeDid = "";
|
|
allContacts: Array<Contact> = [];
|
|
allMyDids: Array<string> = [];
|
|
apiServer = "";
|
|
|
|
amountInput = "0";
|
|
callbackOnSuccess?: (amount: number) => void = () => {};
|
|
customTitle?: string;
|
|
description = "";
|
|
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
|
|
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
|
|
offerId = "";
|
|
prompt = "";
|
|
receiver?: libsUtil.GiverReceiverInputInfo;
|
|
unitCode = "HUR";
|
|
visible = false;
|
|
|
|
libsUtil = libsUtil;
|
|
|
|
projects: PlanData[] = [];
|
|
|
|
didInfo = didInfo;
|
|
|
|
// Computed property to help debug template logic
|
|
get shouldShowProjects() {
|
|
const result =
|
|
(this.stepType === "giver" && this.giverEntityType === "project") ||
|
|
(this.stepType === "recipient" && this.recipientEntityType === "project");
|
|
return result;
|
|
}
|
|
|
|
// Computed property to check if current selection would create a conflict
|
|
get hasPersonConflict() {
|
|
// Only check for conflicts when both entities are persons
|
|
if (
|
|
this.giverEntityType !== "person" ||
|
|
this.recipientEntityType !== "person"
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Check if giver and recipient are the same person
|
|
if (
|
|
this.giver?.did &&
|
|
this.receiver?.did &&
|
|
this.giver.did === this.receiver.did
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Computed property to check if a contact would create a conflict when selected
|
|
wouldCreateConflict(contactDid: string) {
|
|
// Only check for conflicts when both entities are persons
|
|
if (
|
|
this.giverEntityType !== "person" ||
|
|
this.recipientEntityType !== "person"
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (this.stepType === "giver") {
|
|
// If selecting as giver, check if it conflicts with current recipient
|
|
return this.receiver?.did === contactDid;
|
|
} else if (this.stepType === "recipient") {
|
|
// If selecting as recipient, check if it conflicts with current giver
|
|
return this.giver?.did === contactDid;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
stepType = "giver";
|
|
giverEntityType = "person" as "person" | "project";
|
|
recipientEntityType = "person" as "person" | "project";
|
|
|
|
updateEntityTypes() {
|
|
// Reset and set entity types based on current context
|
|
this.giverEntityType = "person";
|
|
this.recipientEntityType = "person";
|
|
|
|
// Determine entity types based on current context
|
|
if (this.showProjects) {
|
|
// HomeView "Project" button or ProjectViewView "Given by This"
|
|
this.giverEntityType = "project";
|
|
this.recipientEntityType = "person";
|
|
} else if (this.fromProjectId) {
|
|
// ProjectViewView "Given by This" button (project is giver)
|
|
this.giverEntityType = "project";
|
|
this.recipientEntityType = "person";
|
|
} else if (this.toProjectId) {
|
|
// ProjectViewView "Given to This" button (project is recipient)
|
|
this.giverEntityType = "person";
|
|
this.recipientEntityType = "project";
|
|
} else {
|
|
// HomeView "Person" button
|
|
this.giverEntityType = "person";
|
|
this.recipientEntityType = "person";
|
|
}
|
|
}
|
|
|
|
async open(
|
|
giver?: libsUtil.GiverReceiverInputInfo,
|
|
receiver?: libsUtil.GiverReceiverInputInfo,
|
|
offerId?: string,
|
|
customTitle?: string,
|
|
prompt?: string,
|
|
callbackOnSuccess: (amount: number) => void = () => {},
|
|
) {
|
|
this.customTitle = customTitle;
|
|
this.giver = giver;
|
|
this.prompt = prompt || "";
|
|
this.receiver = receiver;
|
|
this.amountInput = "0";
|
|
this.callbackOnSuccess = callbackOnSuccess;
|
|
this.offerId = offerId || "";
|
|
this.firstStep = !giver;
|
|
this.stepType = "giver";
|
|
|
|
// Update entity types based on current props
|
|
this.updateEntityTypes();
|
|
|
|
// Update entity types based on current props
|
|
this.updateEntityTypes();
|
|
|
|
try {
|
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
|
this.apiServer = settings.apiServer || "";
|
|
this.activeDid = settings.activeDid || "";
|
|
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
|
if (result) {
|
|
this.allContacts = databaseUtil.mapQueryResultToValues(
|
|
result,
|
|
) as unknown as Contact[];
|
|
}
|
|
|
|
this.allMyDids = await retrieveAccountDids();
|
|
|
|
if (this.giver && !this.giver.name) {
|
|
this.giver.name = didInfo(
|
|
this.giver.did,
|
|
this.activeDid,
|
|
this.allMyDids,
|
|
this.allContacts,
|
|
);
|
|
}
|
|
|
|
if (
|
|
this.giverEntityType === "project" ||
|
|
this.recipientEntityType === "project"
|
|
) {
|
|
await this.loadProjects();
|
|
} else {
|
|
// Clear projects array when not needed
|
|
this.projects = [];
|
|
}
|
|
} catch (err: unknown) {
|
|
logger.error("Error retrieving settings from database:", err);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text:
|
|
err instanceof Error
|
|
? err.message
|
|
: "There was an error retrieving your settings.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
this.visible = true;
|
|
}
|
|
|
|
close() {
|
|
// close the dialog but don't change values (since it might be submitting info)
|
|
this.visible = false;
|
|
}
|
|
|
|
changeUnitCode() {
|
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
|
const index = units.indexOf(this.unitCode);
|
|
this.unitCode = units[(index + 1) % units.length];
|
|
}
|
|
|
|
increment() {
|
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
|
}
|
|
|
|
decrement() {
|
|
this.amountInput = `${Math.max(
|
|
0,
|
|
(parseFloat(this.amountInput) || 1) - 1,
|
|
)}`;
|
|
}
|
|
|
|
cancel() {
|
|
this.close();
|
|
this.eraseValues();
|
|
}
|
|
|
|
eraseValues() {
|
|
this.description = "";
|
|
this.giver = undefined;
|
|
this.amountInput = "0";
|
|
this.prompt = "";
|
|
this.unitCode = "HUR";
|
|
this.firstStep = true;
|
|
}
|
|
|
|
async confirm() {
|
|
if (!this.activeDid) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "You must select an identifier before you can record a give.",
|
|
},
|
|
3000,
|
|
);
|
|
return;
|
|
}
|
|
if (parseFloat(this.amountInput) < 0) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
text: "You may not send a negative number.",
|
|
title: "",
|
|
},
|
|
2000,
|
|
);
|
|
return;
|
|
}
|
|
if (!this.description && !parseFloat(this.amountInput)) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: `You must enter a description or some number of ${
|
|
this.libsUtil.UNIT_LONG[this.unitCode]
|
|
}.`,
|
|
},
|
|
2000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check for person conflict
|
|
if (this.hasPersonConflict) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "You cannot select the same person as both giver and recipient.",
|
|
},
|
|
3000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check for person conflict
|
|
if (this.hasPersonConflict) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "You cannot select the same person as both giver and recipient.",
|
|
},
|
|
3000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.close();
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "toast",
|
|
text: "Recording the give...",
|
|
title: "",
|
|
},
|
|
1000,
|
|
);
|
|
// this is asynchronous, but we don't need to wait for it to complete
|
|
await this.recordGive(
|
|
(this.giver?.did as string) || null,
|
|
(this.receiver?.did as string) || null,
|
|
this.description,
|
|
parseFloat(this.amountInput),
|
|
this.unitCode,
|
|
).then(() => {
|
|
this.eraseValues();
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param giverDid may be null
|
|
* @param recipientDid may be null
|
|
* @param description may be an empty string
|
|
* @param amount may be 0
|
|
* @param unitCode may be omitted, defaults to "HUR"
|
|
*/
|
|
async recordGive(
|
|
giverDid: string | null,
|
|
recipientDid: string | null,
|
|
description: string,
|
|
amount: number,
|
|
unitCode: string = "HUR",
|
|
) {
|
|
try {
|
|
// Determine the correct parameters based on entity types
|
|
let fromDid: string | undefined;
|
|
let toDid: string | undefined;
|
|
let fulfillsProjectHandleId: string | undefined;
|
|
let providerPlanHandleId: string | undefined;
|
|
|
|
if (
|
|
this.giverEntityType === "project" &&
|
|
this.recipientEntityType === "person"
|
|
) {
|
|
// Project-to-person gift
|
|
fromDid = undefined; // No person giver
|
|
toDid = recipientDid as string; // Person recipient
|
|
fulfillsProjectHandleId = undefined; // No project recipient
|
|
providerPlanHandleId = this.giver?.handleId; // Project giver
|
|
} else if (
|
|
this.giverEntityType === "person" &&
|
|
this.recipientEntityType === "project"
|
|
) {
|
|
// Person-to-project gift
|
|
fromDid = giverDid as string; // Person giver
|
|
toDid = undefined; // No person recipient
|
|
fulfillsProjectHandleId = this.toProjectId; // Project recipient
|
|
providerPlanHandleId = undefined; // No project giver
|
|
} else {
|
|
// Person-to-person gift
|
|
fromDid = giverDid as string;
|
|
toDid = recipientDid as string;
|
|
fulfillsProjectHandleId = undefined;
|
|
providerPlanHandleId = undefined;
|
|
}
|
|
|
|
const result = await createAndSubmitGive(
|
|
this.axios,
|
|
this.apiServer,
|
|
this.activeDid,
|
|
fromDid,
|
|
toDid,
|
|
description,
|
|
amount,
|
|
unitCode,
|
|
fulfillsProjectHandleId,
|
|
this.offerId,
|
|
false,
|
|
undefined,
|
|
providerPlanHandleId,
|
|
);
|
|
|
|
if (!result.success) {
|
|
const errorMessage = this.getGiveCreationErrorMessage(result);
|
|
logger.error("Error with give creation result:", result);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: errorMessage || "There was an error creating the give.",
|
|
},
|
|
-1,
|
|
);
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Success",
|
|
text: `That gift was recorded.`,
|
|
},
|
|
7000,
|
|
);
|
|
if (this.callbackOnSuccess) {
|
|
this.callbackOnSuccess(amount);
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
logger.error("Error with give recordation caught:", error);
|
|
const errorMessage =
|
|
error.userMessage ||
|
|
serverMessageForUser(error) ||
|
|
"There was an error recording the give.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: errorMessage,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Helper functions for readability
|
|
|
|
/**
|
|
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
|
* @returns best guess at an error message
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
getGiveCreationErrorMessage(result: any) {
|
|
return (
|
|
result.error?.userMessage ||
|
|
result.error?.error ||
|
|
result.response?.data?.error?.message
|
|
);
|
|
}
|
|
|
|
explainData() {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Data Sharing",
|
|
text: libsUtil.PRIVACY_MESSAGE,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
selectGiver(contact?: Contact) {
|
|
if (contact) {
|
|
this.giver = {
|
|
did: contact.did,
|
|
name: contact.name || contact.did,
|
|
};
|
|
} else {
|
|
this.giver = {
|
|
did: "",
|
|
name: "Unnamed",
|
|
};
|
|
}
|
|
this.firstStep = false;
|
|
}
|
|
|
|
goBackToStep1(step: string) {
|
|
this.stepType = step;
|
|
this.firstStep = true;
|
|
}
|
|
|
|
async loadProjects() {
|
|
try {
|
|
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
|
|
method: "GET",
|
|
headers: await getHeaders(this.activeDid),
|
|
});
|
|
|
|
if (response.status !== 200) {
|
|
throw new Error("Failed to load projects");
|
|
}
|
|
|
|
const results = await response.json();
|
|
if (results.data) {
|
|
this.projects = results.data;
|
|
}
|
|
} catch (error) {
|
|
logger.error("Error loading projects:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Failed to load projects",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
}
|
|
|
|
selectProject(project: PlanData) {
|
|
this.giver = {
|
|
did: project.handleId,
|
|
name: project.name,
|
|
image: project.image,
|
|
handleId: project.handleId,
|
|
};
|
|
this.receiver = {
|
|
did: this.activeDid,
|
|
name: "You",
|
|
};
|
|
this.firstStep = false;
|
|
}
|
|
|
|
selectRecipient(contact?: Contact) {
|
|
if (contact) {
|
|
this.receiver = {
|
|
did: contact.did,
|
|
name: contact.name || contact.did,
|
|
};
|
|
} else {
|
|
this.receiver = {
|
|
did: "",
|
|
name: "Unnamed",
|
|
};
|
|
}
|
|
this.firstStep = false;
|
|
}
|
|
|
|
selectRecipientProject(project: PlanData) {
|
|
this.receiver = {
|
|
did: project.handleId,
|
|
name: project.name,
|
|
image: project.image,
|
|
handleId: project.handleId,
|
|
};
|
|
this.firstStep = false;
|
|
}
|
|
|
|
// Computed property for the query parameters
|
|
get giftedDetailsQuery() {
|
|
return {
|
|
amountInput: this.amountInput,
|
|
description: this.description,
|
|
giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
|
|
giverName: this.giver?.name,
|
|
offerId: this.offerId,
|
|
fulfillsProjectId:
|
|
this.giverEntityType === "person" &&
|
|
this.recipientEntityType === "project"
|
|
? this.toProjectId
|
|
: undefined,
|
|
providerProjectId:
|
|
this.giverEntityType === "project" &&
|
|
this.recipientEntityType === "person"
|
|
? this.giver?.handleId
|
|
: this.fromProjectId,
|
|
recipientDid: this.receiver?.did,
|
|
recipientName: this.receiver?.name,
|
|
unitCode: this.unitCode,
|
|
};
|
|
}
|
|
|
|
// New event handlers for component integration
|
|
|
|
/**
|
|
* Handle entity selection from EntitySelectionStep
|
|
* @param entity - The selected entity (person or project)
|
|
*/
|
|
handleEntitySelected(entity: {
|
|
type: "person" | "project";
|
|
data: Contact | PlanData;
|
|
}) {
|
|
if (entity.type === "person") {
|
|
const contact = entity.data as Contact;
|
|
if (this.stepType === "giver") {
|
|
this.selectGiver(contact);
|
|
} else {
|
|
this.selectRecipient(contact);
|
|
}
|
|
} else {
|
|
const project = entity.data as PlanData;
|
|
if (this.stepType === "giver") {
|
|
this.selectProject(project);
|
|
} else {
|
|
this.selectRecipientProject(project);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle edit entity request from GiftDetailsStep
|
|
* @param entityType - 'giver' or 'recipient'
|
|
*/
|
|
handleEditEntity(entityType: "giver" | "recipient") {
|
|
this.goBackToStep1(entityType);
|
|
}
|
|
|
|
/**
|
|
* Handle form submission from GiftDetailsStep
|
|
*/
|
|
handleSubmit() {
|
|
this.confirm();
|
|
}
|
|
|
|
/**
|
|
* Handle amount update from GiftDetailsStep
|
|
*/
|
|
handleAmountUpdate(newAmount: number) {
|
|
logger.debug("[GiftedDialog] handleAmountUpdate() called", {
|
|
oldAmount: this.amountInput,
|
|
newAmount,
|
|
});
|
|
this.amountInput = newAmount.toString();
|
|
logger.debug("[GiftedDialog] handleAmountUpdate() - amountInput updated", {
|
|
amountInput: this.amountInput,
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.dialog-overlay {
|
|
z-index: 50;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.dialog {
|
|
background-color: white;
|
|
padding: 1rem;
|
|
border-radius: 0.5rem;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
}
|
|
</style>
|
|
|