feat: disallow selection of a person or project if it's already selected on the other side (giver/receiver)

This commit is contained in:
2025-12-17 21:20:27 -07:00
parent 70a0ef7ef6
commit 34a7119086
3 changed files with 144 additions and 21 deletions

View File

@@ -155,6 +155,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
:all-contacts="allContacts" :all-contacts="allContacts"
:conflicted="isProjectConflicted(project.handleId)"
:notify="notify" :notify="notify"
:conflict-context="conflictContext" :conflict-context="conflictContext"
@project-selected="handleProjectSelected" @project-selected="handleProjectSelected"
@@ -175,6 +176,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
:all-contacts="allContacts" :all-contacts="allContacts"
:conflicted="isProjectConflicted(project.handleId)"
:notify="notify" :notify="notify"
:conflict-context="conflictContext" :conflict-context="conflictContext"
@project-selected="handleProjectSelected" @project-selected="handleProjectSelected"
@@ -190,6 +192,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
:all-contacts="allContacts" :all-contacts="allContacts"
:conflicted="isProjectConflicted(project.handleId)"
:notify="notify" :notify="notify"
:conflict-context="conflictContext" :conflict-context="conflictContext"
@project-selected="handleProjectSelected" @project-selected="handleProjectSelected"
@@ -555,6 +558,13 @@ export default class EntityGrid extends Vue {
return this.conflictChecker(did); return this.conflictChecker(did);
} }
/**
* Check if a project handleId is conflicted
*/
isProjectConflicted(handleId: string): boolean {
return this.conflictChecker(handleId);
}
/** /**
* Handle person selection from PersonCard * Handle person selection from PersonCard
*/ */

View File

@@ -179,22 +179,56 @@ export default class GiftedDialog extends Vue {
return false; return false;
} }
// Computed property to check if a contact would create a conflict when selected // Computed property to check if current selection would create a project conflict
wouldCreateConflict(contactDid: string) { get hasProjectConflict() {
// Only check for conflicts when both entities are persons // Only check for conflicts when both entities are projects
if ( if (
this.currentGiverEntityType !== "person" || this.currentGiverEntityType !== "project" ||
this.currentRecipientEntityType !== "person" this.currentRecipientEntityType !== "project"
) { ) {
return false; return false;
} }
// Check if giver and recipient are the same project
if (
this.giver?.handleId &&
this.receiver?.handleId &&
this.giver.handleId === this.receiver.handleId
) {
return true;
}
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 (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient // If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === contactDid; return this.receiver?.did === identifier;
} else if (this.stepType === "recipient") { } else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver // If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === contactDid; 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; return false;
@@ -363,6 +397,15 @@ export default class GiftedDialog extends Vue {
return; return;
} }
// Check for project conflict
if (this.hasProjectConflict) {
this.safeNotify.error(
"You cannot select the same project as both giver and recipient.",
TIMEOUTS.STANDARD,
);
return;
}
this.close(); this.close();
this.safeNotify.toast( this.safeNotify.toast(
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message, NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message,

View File

@@ -1,11 +1,8 @@
/** * ProjectCard.vue - Individual project display component * * Extracted from /** * ProjectCard.vue - Individual project display component * * Extracted from
GiftedDialog.vue to handle project entity display * with selection states and GiftedDialog.vue to handle project entity display * with selection states,
issuer information. * * @author Matthew Raymer */ conflict detection, and issuer information. * * @author Matthew Raymer */
<template> <template>
<li <li :class="cardClasses" @click="handleClick">
class="flex items-center gap-2 px-2 py-1.5 border-b border-slate-300 hover:bg-slate-50 cursor-pointer"
@click="handleClick"
>
<ProjectIcon <ProjectIcon
:entity-id="project.handleId" :entity-id="project.handleId"
:icon-size="30" :icon-size="30"
@@ -14,8 +11,8 @@ issuer information. * * @author Matthew Raymer */
/> />
<div class="overflow-hidden"> <div class="overflow-hidden">
<h3 class="text-sm font-semibold truncate"> <h3 :class="nameClasses">
{{ project.name || unnamedProject }} {{ displayName }}
</h3> </h3>
<div class="text-xs text-slate-500 truncate"> <div class="text-xs text-slate-500 truncate">
@@ -33,6 +30,7 @@ import { PlanData } from "../interfaces/records";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { didInfo } from "../libs/endorserServer"; import { didInfo } from "../libs/endorserServer";
import { UNNAMED_PROJECT } from "@/constants/entities"; import { UNNAMED_PROJECT } from "@/constants/entities";
import { NotificationIface } from "../constants/app";
/** /**
* ProjectCard - Displays a project entity with selection capability * ProjectCard - Displays a project entity with selection capability
@@ -42,6 +40,8 @@ import { UNNAMED_PROJECT } from "@/constants/entities";
* - Displays project name and issuer information * - Displays project name and issuer information
* - Handles click events for selection * - Handles click events for selection
* - Shows issuer name using didInfo utility * - Shows issuer name using didInfo utility
* - Selection states (selectable, conflicted, disabled)
* - Warning notifications for conflicted entities
*/ */
@Component({ @Component({
components: { components: {
@@ -65,6 +65,18 @@ export default class ProjectCard extends Vue {
@Prop({ required: true }) @Prop({ required: true })
allContacts!: Contact[]; allContacts!: Contact[];
/** Whether this project would create a conflict if selected */
@Prop({ default: false })
conflicted!: boolean;
/** Notification function from parent component */
@Prop()
notify?: (notification: NotificationIface, timeout?: number) => void;
/** Context for conflict messages (e.g., "giver", "recipient") */
@Prop({ default: "other party" })
conflictContext!: string;
/** /**
* Get the unnamed project constant * Get the unnamed project constant
*/ */
@@ -72,6 +84,51 @@ export default class ProjectCard extends Vue {
return UNNAMED_PROJECT; return UNNAMED_PROJECT;
} }
/**
* Computed CSS classes for the card
*/
get cardClasses(): string {
const baseCardClasses =
"flex items-center gap-2 px-2 py-1.5 border-b border-slate-300";
if (this.conflicted) {
return `${baseCardClasses} *:opacity-50 cursor-not-allowed`;
}
return `${baseCardClasses} cursor-pointer hover:bg-slate-50`;
}
/**
* Computed CSS classes for the project name
*/
get nameClasses(): string {
const baseNameClasses = "text-sm font-semibold truncate";
if (this.conflicted) {
return `${baseNameClasses} text-slate-500`;
}
// Add italic styling for entities without set names
if (!this.project.name) {
return `${baseNameClasses} italic text-slate-500`;
}
return baseNameClasses;
}
/**
* Computed display name for the project
*/
get displayName(): string {
// If the project has a set name, use that name
if (this.project.name) {
return this.project.name;
}
// If the project does not have a set name
return this.unnamedProject;
}
/** /**
* Computed display name for the project issuer * Computed display name for the project issuer
*/ */
@@ -85,10 +142,23 @@ export default class ProjectCard extends Vue {
} }
/** /**
* Handle card click - emit project selection * Handle card click - emit if not conflicted, show warning if conflicted
*/ */
handleClick(): void { handleClick(): void {
if (!this.conflicted) {
this.emitProjectSelected(this.project); this.emitProjectSelected(this.project);
} else if (this.notify) {
// Show warning notification for conflicted entity
this.notify(
{
group: "alert",
type: "warning",
title: "Cannot Select",
text: `You cannot select "${this.displayName}" because it is already selected as the ${this.conflictContext}.`,
},
3000,
);
}
} }
// Emit methods using @Emit decorator // Emit methods using @Emit decorator