feat: allow changing the giver when they get to the give-detail screen

This commit is contained in:
2026-01-18 19:50:55 -07:00
parent 46f2cbfcc6
commit b500a1e7c0
4 changed files with 261 additions and 249 deletions

View File

@@ -87,18 +87,6 @@ export default class EntitySelectionStep extends Vue {
@Prop({ required: true })
stepType!: "giver" | "recipient";
/** Type of giver entity: 'person' or 'project' */
@Prop({ required: true })
giverEntityType!: "person" | "project";
/** Type of recipient entity: 'person' or 'project' */
@Prop({ required: true })
recipientEntityType!: "person" | "project";
/** Whether to show projects instead of people */
@Prop({ default: false })
showProjects!: boolean;
/** Array of available projects (optional - EntityGrid loads internally if not provided) */
@Prop({ required: false })
projects?: PlanData[];
@@ -153,6 +141,10 @@ export default class EntitySelectionStep extends Vue {
@Prop()
notify?: (notification: NotificationIface, timeout?: number) => void;
// initializing based on a Prop doesn't work here; see "mounted()"
newGiverEntityType: "person" | "project" = "person";
newRecipientEntityType: "person" | "project" = "person";
/**
* CSS classes for the cancel button
*/
@@ -195,11 +187,10 @@ export default class EntitySelectionStep extends Vue {
* Whether to show projects in the grid
*/
get shouldShowProjects(): boolean {
// When editing an entity, show the appropriate entity type for that entity
if (this.stepType === "giver") {
return this.giverEntityType === "project";
return this.newGiverEntityType === "project";
} else if (this.stepType === "recipient") {
return this.recipientEntityType === "project";
return this.newRecipientEntityType === "project";
}
return false;
}
@@ -222,6 +213,13 @@ export default class EntitySelectionStep extends Vue {
}
}
async mounted(): Promise<void> {
this.newGiverEntityType = this.giver?.handleId ? "project" : "person";
this.newRecipientEntityType = this.receiver?.handleId
? "project"
: "person";
}
/**
* Handle entity selection from EntityGrid
*/
@@ -236,7 +234,13 @@ export default class EntitySelectionStep extends Vue {
* Handle toggle entity type button click
*/
handleToggleEntityType(): void {
this.emitToggleEntityType();
if (this.stepType === "giver") {
this.newGiverEntityType =
this.newGiverEntityType === "person" ? "project" : "person";
} else if (this.stepType === "recipient") {
this.newRecipientEntityType =
this.newRecipientEntityType === "person" ? "project" : "person";
}
}
/**
@@ -259,11 +263,6 @@ export default class EntitySelectionStep extends Vue {
emitCancel(): void {
// No return value needed
}
@Emit("toggle-entity-type")
emitToggleEntityType(): void {
// No return value needed
}
}
</script>

View File

@@ -104,7 +104,6 @@ import { Component, Prop, Vue, Watch, Emit } from "vue-facing-decorator";
import EntitySummaryButton from "./EntitySummaryButton.vue";
import AmountInput from "./AmountInput.vue";
import { RouteLocationRaw } from "vue-router";
import { logger } from "@/utils/logger";
/**
* Entity data interface for giver/receiver
@@ -290,20 +289,12 @@ export default class GiftDetailsStep extends Vue {
query: {
amountInput: this.localAmount.toString(),
description: this.localDescription,
giverDid:
this.giverEntityType === "person" ? this.giver?.did : undefined,
giverDid: this.giver?.did,
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId:
this.recipientEntityType === "project" ? this.toProjectId : undefined,
providerProjectId:
this.giverEntityType === "project"
? this.giver?.handleId
: this.fromProjectId,
recipientDid:
this.recipientEntityType === "person"
? this.receiver?.did
: undefined,
fulfillsProjectId: this.receiver?.handleId,
providerProjectId: this.giver?.handleId,
recipientDid: this.receiver?.did,
recipientName: this.receiver?.name,
unitCode: this.localUnitCode,
},
@@ -323,10 +314,6 @@ export default class GiftDetailsStep extends Vue {
* Calls the onUpdateAmount function prop for parent control
*/
handleAmountChange(newAmount: number): void {
logger.debug("[GiftDetailsStep] handleAmountChange() called", {
oldAmount: this.localAmount,
newAmount,
});
this.localAmount = newAmount;
this.onUpdateAmount(newAmount);
}

View File

@@ -29,7 +29,6 @@
:offer-id="offerId"
:notify="$notify"
@entity-selected="handleEntitySelected"
@toggle-entity-type="handleToggleEntityType"
@cancel="cancel"
/>
@@ -187,39 +186,6 @@ export default class GiftedDialog extends Vue {
return false;
}
// Computed property to check if a contact or project would create a conflict when selected
wouldCreateConflict(identifier: string) {
// Check for person conflicts when both entities are persons
if (
this.currentGiverEntityType === "person" &&
this.currentRecipientEntityType === "person"
) {
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === identifier;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === identifier;
}
}
// Check for project conflicts when both entities are projects
if (
this.currentGiverEntityType === "project" &&
this.currentRecipientEntityType === "project"
) {
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.handleId === identifier;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.handleId === identifier;
}
}
return false;
}
async open(
giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo,
@@ -307,15 +273,50 @@ export default class GiftedDialog extends Vue {
this.eraseValues();
}
// Computed property to check if a contact or project would create a conflict when selected
wouldCreateConflict(identifier: string) {
// Check for person conflicts when both entities are persons
if (
this.currentGiverEntityType === "person" &&
this.currentRecipientEntityType === "person"
) {
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === identifier;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === identifier;
}
}
// Check for project conflicts when both entities are projects
if (
this.currentGiverEntityType === "project" &&
this.currentRecipientEntityType === "project"
) {
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.handleId === identifier;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.handleId === identifier;
}
}
return false;
}
eraseValues() {
this.description = "";
this.giver = undefined;
this.receiver = undefined;
this.amountInput = "0";
this.prompt = "";
this.unitCode = "HUR";
this.firstStep = true;
// Reset to initial prop values
this.currentGiverEntityType = this.initialGiverEntityType;
this.currentRecipientEntityType = this.initialRecipientEntityType;
}
async confirm() {
@@ -407,25 +408,34 @@ export default class GiftedDialog extends Vue {
this.currentRecipientEntityType === "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
fromDid = undefined;
toDid = recipientDid as string;
fulfillsProjectHandleId = undefined;
providerPlanHandleId = this.giver?.handleId;
} else if (
this.currentGiverEntityType === "person" &&
this.currentRecipientEntityType === "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 {
fromDid = giverDid as string;
toDid = undefined;
fulfillsProjectHandleId = this.receiver?.handleId;
providerPlanHandleId = undefined;
} else if (
this.currentGiverEntityType === "person" &&
this.currentRecipientEntityType === "person"
) {
// Person-to-person gift
fromDid = giverDid as string;
toDid = recipientDid as string;
fulfillsProjectHandleId = undefined;
providerPlanHandleId = undefined;
} else {
// Project-to-project gift
fromDid = undefined;
toDid = undefined;
fulfillsProjectHandleId = this.receiver?.handleId;
providerPlanHandleId = this.giver?.handleId;
}
const result = await createAndSubmitGive(
@@ -496,7 +506,16 @@ export default class GiftedDialog extends Vue {
this.safeNotify.info(libsUtil.PRIVACY_MESSAGE, TIMEOUTS.MODAL);
}
selectGiver(contact?: Contact) {
goBackToStep1(step: string) {
this.stepType = step;
this.firstStep = true;
}
moveToStep2() {
this.firstStep = false;
}
selectGiverPerson(contact?: Contact) {
if (contact) {
this.giver = {
did: contact.did,
@@ -514,33 +533,16 @@ export default class GiftedDialog extends Vue {
this.firstStep = false;
}
goBackToStep1(step: string) {
this.stepType = step;
this.firstStep = true;
}
moveToStep2() {
this.firstStep = false;
}
selectProject(project: PlanData) {
selectGiverProject(project: PlanData) {
this.giver = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
// Only set receiver to "You" if no receiver has been selected yet
if (!this.receiver || !this.receiver.did) {
this.receiver = {
did: this.activeDid,
name: "You",
};
}
this.firstStep = false;
}
selectRecipient(contact?: Contact) {
selectRecipientPerson(contact?: Contact) {
if (contact) {
this.receiver = {
did: contact.did,
@@ -560,7 +562,6 @@ export default class GiftedDialog extends Vue {
selectRecipientProject(project: PlanData) {
this.receiver = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
@@ -568,32 +569,6 @@ export default class GiftedDialog extends Vue {
this.firstStep = false;
}
// Computed property for the query parameters
get giftedDetailsQuery() {
return {
amountInput: this.amountInput,
description: this.description,
giverDid:
this.currentGiverEntityType === "person" ? this.giver?.did : undefined,
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId:
this.currentRecipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId:
this.currentGiverEntityType === "project"
? this.giver?.handleId
: this.fromProjectId,
recipientDid:
this.currentRecipientEntityType === "person"
? this.receiver?.did
: undefined,
recipientName: this.receiver?.name,
unitCode: this.unitCode,
};
}
// New event handlers for component integration
/**
@@ -610,14 +585,14 @@ export default class GiftedDialog extends Vue {
// Apply DID-based logic for person entities
const processedContact = this.processPersonEntity(contact);
if (entity.stepType === "giver") {
this.selectGiver(processedContact);
this.selectGiverPerson(processedContact);
} else {
this.selectRecipient(processedContact);
this.selectRecipientPerson(processedContact);
}
} else if (entity.type === "project") {
const project = entity.data as PlanData;
if (entity.stepType === "giver") {
this.selectProject(project);
this.selectGiverProject(project);
} else {
this.selectRecipientProject(project);
}
@@ -659,24 +634,6 @@ export default class GiftedDialog extends Vue {
this.confirm();
}
/**
* Handle toggle entity type request from EntitySelectionStep
*/
handleToggleEntityType() {
// Toggle the appropriate entity type based on current step
if (this.stepType === "giver") {
this.currentGiverEntityType =
this.currentGiverEntityType === "person" ? "project" : "person";
// Clear any selected giver when toggling
this.giver = undefined;
} else if (this.stepType === "recipient") {
this.currentRecipientEntityType =
this.currentRecipientEntityType === "person" ? "project" : "person";
// Clear any selected receiver when toggling
this.receiver = undefined;
}
}
/**
* Handle amount update from GiftDetailsStep
*/

View File

@@ -31,9 +31,10 @@
<div class="truncate">
From
{{
providedByProject
givenByProjectFunction()
? providerProjectName
: providedByGiver
: // check for DID because name could be "Unnamed"
givenByPersonFunction() && giverDid
? giverName
: "someone not named"
}}
@@ -104,57 +105,46 @@
<div
class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden"
>
<div class="flex items-center">
<input
v-if="giverDid && !providedByProject"
v-model="providedByGiver"
type="checkbox"
class="flex-shrink-0 h-6 w-6 mr-2"
/>
<font-awesome
v-else
icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/>
<label class="text-sm truncate">
{{
giverDid
? "This was provided by " + giverName + "."
: "No named individual gave."
}}
</label>
<font-awesome
v-if="!giverDid || providedByProject"
icon="info-circle"
class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserOfGiver()"
/>
<!-- Giver Selection Display -->
<div
v-if="!showGiverSelection"
class="cursor-pointer"
@click="openGiverSelection"
>
<div class="flex items-center">
<label class="text-sm flex-1">
{{
givenByProjectFunction() && providerProjectName
? "From " + providerProjectName
: givenByPersonFunction() && giverName
? "From " + giverName
: "Unnamed giver"
}}
</label>
<span class="text-sm text-blue-500">Change</span>
<font-awesome icon="chevron-right" class="text-blue-500 ms-2" />
</div>
</div>
<div class="flex items-center">
<input
v-if="providerProjectId && !providedByGiver"
v-model="providedByProject"
type="checkbox"
class="flex-shrink-0 h-6 w-6 mr-2"
/>
<font-awesome
v-else
icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/>
<label class="text-sm truncate">
{{
providerProjectId
? "This was provided by " + providerProjectName + "."
: "This was not provided by a project."
}}
</label>
<font-awesome
v-if="!providerProjectId || providedByGiver"
icon="info-circle"
class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserOfProvidingProject()"
<!-- Giver Selection Interface -->
<div v-if="showGiverSelection">
<EntitySelectionStep
step-type="giver"
:all-contacts="allContacts"
:active-did="activeDid"
:all-my-dids="allMyDids"
:conflict-checker="wouldCreateConflict"
:from-project-id="providerProjectId"
:to-project-id="fulfillsProjectId"
:giver="currentGiver"
:receiver="currentReceiver"
:description="description"
:amount-input="amountInput"
:unit-code="unitCode"
:offer-id="offerId"
:notify="$notify"
@entity-selected="handleGiverEntitySelected"
@cancel="closeGiverSelection"
/>
</div>
</div>
@@ -273,6 +263,7 @@ import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import AmountInput from "../components/AmountInput.vue";
import EntitySelectionStep from "../components/EntitySelectionStep.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import { GenericCredWrapper, GiveActionClaim } from "../interfaces";
import {
@@ -287,6 +278,7 @@ import * as libsUtil from "../libs/util";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { Contact } from "@/db/tables/contacts";
import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
@@ -308,6 +300,7 @@ import {
QuickNav,
TopMessage,
AmountInput,
EntitySelectionStep,
},
mixins: [PlatformServiceMixin],
})
@@ -319,6 +312,8 @@ export default class GiftedDetails extends Vue {
activeDid = "";
apiServer = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
amountInput = "0";
description = "";
@@ -336,11 +331,10 @@ export default class GiftedDetails extends Vue {
prevCredToEdit?: GenericCredWrapper<GiveActionClaim>;
providerProjectId = "";
providerProjectName = "a project";
providedByProject = false; // basically static, based on input; if we allow changing then let's fix things (see below)
providedByGiver = false; // basically static, based on input; if we allow changing then let's fix things (see below)
recipientDid = "";
recipientName = "";
showGeneralAdvanced = false;
showGiverSelection = false;
unitCode = "HUR";
libsUtil = libsUtil;
@@ -457,29 +451,30 @@ export default class GiftedDetails extends Vue {
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
const dbContacts = await this.$dbQuery("SELECT * FROM contacts");
this.allContacts = this.$mapQueryResultToValues(
dbContacts,
) as unknown as Contact[];
this.allMyDids = await retrieveAccountDids();
if (
(this.giverDid && !this.giverName) ||
(this.recipientDid && !this.recipientName)
) {
const dbContacts = await this.$dbQuery("SELECT * FROM contacts");
const allContacts = this.$mapQueryResultToValues(
dbContacts,
) as unknown as Contact[];
const allMyDids = await retrieveAccountDids();
if (this.giverDid && !this.giverName) {
this.giverName = didInfo(
this.giverDid,
this.activeDid,
allMyDids,
allContacts,
this.allMyDids,
this.allContacts,
);
}
if (this.recipientDid && !this.recipientName) {
this.recipientName = didInfo(
this.recipientDid,
this.activeDid,
allMyDids,
allContacts,
this.allMyDids,
this.allContacts,
);
}
}
@@ -487,10 +482,6 @@ export default class GiftedDetails extends Vue {
this.givenToProject = !!this.fulfillsProjectId;
this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
// these should be functions but something's wrong with the syntax in the <> conditional
this.providedByProject = !!this.providerProjectId;
this.providedByGiver = !this.providedByProject && !!this.giverDid;
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
if (this.fulfillsProjectId) {
@@ -517,6 +508,22 @@ export default class GiftedDetails extends Vue {
}
}
givenByPersonFunction() {
return !!this.giverDid;
}
givenByProjectFunction() {
return !!this.providerProjectId;
}
givenToPersonFunction() {
return !!this.recipientDid;
}
givenToProjectFunction() {
return !!this.fulfillsProjectId;
}
changeUnitCode() {
const units = Object.keys(this.libsUtil.UNIT_SHORT);
const index = units.indexOf(this.unitCode);
@@ -655,22 +662,6 @@ export default class GiftedDetails extends Vue {
await this.recordGive();
}
notifyUserOfGiver() {
// there's no individual giver or there's a provider project
if (!this.giverDid) {
this.notify.warning(
"To assign a giver, you must choose a person in a previous step.",
TIMEOUTS.SHORT,
);
} else {
// must be because providedByProject is true
this.notify.warning(
"You cannot assign both a giver and a project.",
TIMEOUTS.SHORT,
);
}
}
notifyUserOfRecipient() {
// there's no individual recipient or there's a fulfills project
if (!this.recipientDid) {
@@ -728,13 +719,6 @@ export default class GiftedDetails extends Vue {
*/
public async recordGive() {
try {
const giverDid = this.providedByGiver ? this.giverDid : undefined;
const recipientDid = this.givenToRecipient
? this.recipientDid
: undefined;
const fulfillsProjectId = this.givenToProject
? this.fulfillsProjectId
: undefined;
let result;
if (this.prevCredToEdit) {
// don't create from a blank one in case some properties were set from a different interface
@@ -743,12 +727,12 @@ export default class GiftedDetails extends Vue {
this.apiServer,
this.prevCredToEdit,
this.activeDid,
giverDid,
recipientDid,
this.giverDid,
this.recipientDid,
this.description,
parseFloat(this.amountInput),
this.unitCode,
fulfillsProjectId,
this.fulfillsProjectId,
this.offerId,
false,
this.imageUrl,
@@ -812,19 +796,14 @@ export default class GiftedDetails extends Vue {
}
constructGiveParam() {
const giverDid = this.providedByGiver ? this.giverDid : undefined;
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
const fulfillsProjectId = this.givenToProject
? this.fulfillsProjectId
: undefined;
const giveClaim = hydrateGive(
this.prevCredToEdit?.claim as GiveActionClaim,
giverDid,
recipientDid,
this.giverDid,
this.recipientDid,
this.description,
parseFloat(this.amountInput),
this.unitCode,
fulfillsProjectId,
this.fulfillsProjectId,
this.offerId,
false,
this.imageUrl,
@@ -845,5 +824,95 @@ export default class GiftedDetails extends Vue {
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
}
/**
* Computed property for current giver entity data
*/
get currentGiver() {
if (this.providerProjectId) {
return {
handleId: this.providerProjectId,
name: this.providerProjectName,
};
} else if (this.giverDid) {
return {
did: this.giverDid,
name: this.giverName,
};
}
return undefined;
}
/**
* Computed property for current receiver entity data
*/
get currentReceiver() {
if (this.givenToProject && this.fulfillsProjectId) {
return {
handleId: this.fulfillsProjectId,
name: this.fulfillsProjectName,
};
} else if (this.givenToRecipient && this.recipientDid) {
return {
did: this.recipientDid,
name: this.recipientName,
};
}
return undefined;
}
/**
* Open giver selection interface
*/
openGiverSelection() {
this.showGiverSelection = true;
}
/**
* Close giver selection interface
*/
closeGiverSelection() {
this.showGiverSelection = false;
}
/**
* Handle giver entity selection
*/
handleGiverEntitySelected(entity: {
type: "person" | "project";
data: Contact | PlanData;
stepType: "giver" | "recipient";
}) {
if (entity.type === "person") {
const contact = entity.data as Contact;
this.giverDid = contact.did;
this.giverName = contact.name || "";
this.providedByGiver = true;
this.providerProjectId = "";
} else if (entity.type === "project") {
const project = entity.data as PlanData;
this.providerProjectId = project.handleId || "";
this.providerProjectName = project.name
? `the project "${project.name}"`
: "a project";
this.providedByGiver = false;
this.giverDid = "";
}
this.showGiverSelection = false;
}
/**
* Check if selecting an entity would create a conflict
*/
wouldCreateConflict(identifier: string): boolean {
// Check if it would conflict with recipient
if (this.givenToRecipient && this.recipientDid === identifier) {
return true;
}
if (this.givenToProject && this.fulfillsProjectId === identifier) {
return true;
}
return false;
}
}
</script>