# GiftedDialog UI Logic Flow
**Author:** Matthew Raymer
**Date:** December 2025
**Component:** `src/components/GiftedDialog.vue`
## Overview
The `GiftedDialog` component is a multi-step modal dialog that facilitates recording gifts/contributions between people and projects in the Time Safari application. It supports various gift scenarios including person-to-person, person-to-project, and project-to-person transactions.
This component serves as the primary interface for users to acknowledge and record contributions they have received or given. When a user wants to document a gift, favor, or contribution, they interact with this dialog which guides them through a structured process to ensure all necessary information is captured accurately. The dialog intelligently adapts its interface based on the context from which it was opened, whether from the home screen, a project page, or another part of the application.
## Architecture
### Component Structure
- **Framework:** Vue 3 with TypeScript using `vue-facing-decorator`
- **Dependencies:**
- Database layer (`db/index.ts`, `databaseUtil.ts`)
- Endorser server integration (`libs/endorserServer.ts`)
- Platform services (`PlatformServiceFactory`)
- Child components: `EntityIcon`, `ProjectIcon`
The component is built using Vue 3's Composition API with TypeScript decorators for a class-based approach. This architectural choice provides strong typing and clear separation of concerns. The component heavily relies on the application's database layer for storing and retrieving contact information, while the endorser server integration handles the cryptographic signing and publishing of gift records to the distributed network.
### Key Properties
```typescript
@Prop() fromProjectId: string // Project ID when project is the giver
@Prop() toProjectId: string // Project ID when project is the recipient
@Prop() showProjects: boolean // Whether to show projects in selection
@Prop() isFromProjectView: boolean // Whether opened from project view
```
These properties control the dialog's behavior and determine what type of entities (people or projects) should be displayed in the selection interface. The `fromProjectId` and `toProjectId` properties are mutually exclusive and indicate whether a specific project is involved in the transaction. When `showProjects` is true, the dialog prioritizes showing available projects rather than people. The `isFromProjectView` flag helps the component understand the navigation context and adjust its interface accordingly.
## Logic Flow
### 1. Initialization Phase
#### Entry Point: `open()` Method
```typescript
async open(
giver?: GiverReceiverInputInfo,
receiver?: GiverReceiverInputInfo,
offerId?: string,
customTitle?: string,
prompt?: string,
callbackOnSuccess: (amount: number) => void = () => {}
)
```
The dialog's lifecycle begins when the `open()` method is called from a parent component. This method accepts several optional parameters that pre-populate the dialog with context-specific information. If a giver or receiver is already known (such as when recording a gift from a specific project), these can be passed in to skip the selection step. The method also accepts custom prompts and titles to provide contextual guidance to the user.
**Initialization Steps:**
1. **Entity Type Determination** (`updateEntityTypes()`)
- Analyzes props to determine if giver/recipient should be person or project
- Sets `giverEntityType` and `recipientEntityType` accordingly
The first step in initialization involves analyzing the component's properties to determine what types of entities should be involved in this gift transaction. This analysis considers the context from which the dialog was opened and sets internal flags that control the subsequent user interface presentation.
2. **Database Setup**
- Retrieves active account settings
- Loads contacts from database
- Fetches user's DIDs for identification
Next, the component establishes its data foundation by connecting to the local database and retrieving the user's contacts, account settings, and decentralized identifiers (DIDs). This information is essential for populating the selection interface and ensuring the user can choose from their known contacts and projects.
3. **Project Loading** (if needed)
- Calls `loadProjects()` if either entity type is "project"
- Fetches available projects from API server
If the dialog needs to display projects (either as potential givers or recipients), it makes an API call to the endorser server to retrieve the current list of available projects. This ensures users see the most up-to-date project information when making their selections.
4. **Step Determination**
- If giver is pre-selected: starts at Step 2 (Gift Details)
- Otherwise: starts at Step 1 (Entity Selection)
Finally, the component determines which step to display first. If enough context was provided during opening (such as a pre-selected giver), the dialog can skip directly to the gift details step. Otherwise, it starts with the entity selection step to gather the necessary information from the user.
### 2. Entity Type Logic
#### Entity Type Matrix
| Context | Giver Type | Recipient Type | Description |
|---------|------------|----------------|-------------|
| `showProjects=true` | project | person | HomeView "Project" button |
| `fromProjectId` set | project | person | Project giving to person |
| `toProjectId` set | person | project | Person giving to project |
| Default | person | person | Person-to-person gift |
The entity type determination follows a clear hierarchy of rules based on the dialog's opening context. When users click the "Project" button from the home view, they're indicating they want to record receiving something from a project, so the dialog configures itself for project-to-person gifts. Conversely, when opened from a project page with a "give to this project" context, it sets up for person-to-project transactions.
#### Dynamic Entity Resolution
```typescript
updateEntityTypes() {
// Reset defaults
this.giverEntityType = "person";
this.recipientEntityType = "person";
// Apply context-specific logic
if (this.showProjects) {
this.giverEntityType = "project";
} else if (this.fromProjectId) {
this.giverEntityType = "project";
} else if (this.toProjectId) {
this.recipientEntityType = "project";
}
}
```
The `updateEntityTypes()` method implements this logic by first resetting both entity types to "person" (the most common case), then applying specific rules based on the component's properties. This approach ensures a predictable fallback while allowing for context-specific customization. The method is called whenever relevant properties change, ensuring the interface stays synchronized with the current context.
### 3. Step 1: Entity Selection
#### Display Logic
The UI dynamically shows different entity grids based on `shouldShowProjects`:
```typescript
get shouldShowProjects() {
return (this.stepType === "giver" && this.giverEntityType === "project") ||
(this.stepType === "recipient" && this.recipientEntityType === "project");
}
```
The first step presents users with a grid of selectable entities, but the content of this grid changes dramatically based on whether projects or people should be displayed. The `shouldShowProjects` computed property determines this by checking if the current selection step (giver or recipient) corresponds to an entity type of "project". This ensures users only see relevant options for their current selection task.
#### Project Selection Grid
- **Layout:** 3-4 columns on mobile/desktop
- **Content:** First 7 projects with icons, names, and issuer info
- **Actions:**
- Click project → `selectProject()` or `selectRecipientProject()`
- "Show All" link → Navigate to discover page
When displaying projects, the interface uses a wider grid layout to accommodate project icons and additional information like the project creator's identity. The grid shows only the first seven projects to avoid overwhelming the user, with a "Show All" option that navigates to the main discovery page for browsing the complete project catalog. Each project is displayed with its custom icon (if available), name, and information about who created or manages the project.
#### Person Selection Grid
- **Layout:** 4-6 columns responsive
- **Content:**
- "You" option (with conflict detection)
- "Unnamed" option
- First 10 contacts with avatars and names
- **Conflict Prevention:**
- Grays out options that would create giver=recipient conflicts
- Uses `wouldCreateConflict()` method for validation
The person selection grid uses a more compact layout since person entries require less information. It always includes a "You" option (allowing users to select themselves) and an "Unnamed" option for cases where the other party isn't in their contact list. The grid then displays up to 10 of the user's contacts, each with their avatar and name. Importantly, the interface includes conflict prevention logic that visually disables options that would result in the same person being selected as both giver and recipient.
#### Conflict Detection Algorithm
```typescript
wouldCreateConflict(contactDid: string) {
// Only applies to person-to-person gifts
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
if (this.stepType === "giver") {
return this.receiver?.did === contactDid;
} else if (this.stepType === "recipient") {
return this.giver?.did === contactDid;
}
return false;
}
```
The conflict detection algorithm prevents users from accidentally selecting the same person for both roles in a gift transaction. It only applies to person-to-person gifts, since project-to-person or person-to-project gifts cannot have this type of conflict. The method checks whether selecting a particular contact would create a duplicate selection by comparing their DID (decentralized identifier) with the already-selected entity for the opposite role.
### 4. Step 2: Gift Details
#### Entity Display
- **Editable Entities:** Show edit button, allow clicking to return to Step 1
- **Locked Entities:** Show lock icon, prevent editing (e.g., when project is pre-selected)
- **Visual Representation:** Appropriate icons (EntityIcon for people, ProjectIcon for projects)
The second step displays the selected entities prominently at the top of the form, providing visual confirmation of the user's choices. The interface distinguishes between editable and locked entities - editable entities show a pencil icon and can be clicked to return to the selection step, while locked entities (such as when the dialog was opened with a specific project context) show a lock icon and cannot be changed. This prevents users from accidentally modifying critical context information.
#### Form Controls
1. **Description Input**
- Text field with dynamic placeholder
- Uses custom prompt if provided
The description field allows users to provide context about what was given or received. The placeholder text adapts based on any custom prompt provided when opening the dialog, helping guide users toward appropriate descriptions for their specific context.
2. **Amount Controls**
- Increment/decrement buttons
- Direct numeric input
- Unit selector (Hours, USD, BTC, BX, ETH)
The amount section provides multiple ways for users to specify the quantity of their gift. Increment and decrement buttons make it easy to adjust whole number amounts, while the direct input field allows for precise decimal values. The unit selector supports various types of contributions, from time (hours) to different currencies, acknowledging that gifts in the Time Safari ecosystem can take many forms.
3. **Advanced Options**
- "Photo & more options" link to `GiftedDetailsView`
- Passes comprehensive query parameters for state preservation
For users who want to add additional details like photos or more complex descriptions, a link navigates to an expanded details view. The component carefully preserves all current form state by encoding it in query parameters, ensuring users don't lose their progress when exploring these advanced options.
#### Validation Logic
```typescript
async confirm() {
// Check for active DID
if (!this.activeDid) {
this.$notify({ type: "danger", text: "You must select an identifier..." });
return;
}
// Validate amount
if (parseFloat(this.amountInput) < 0) {
this.$notify({ type: "danger", text: "You may not send a negative number." });
return;
}
// Require description or amount
if (!this.description && !parseFloat(this.amountInput)) {
this.$notify({ type: "danger", text: "You must enter a description or amount." });
return;
}
// Check for person conflict
if (this.hasPersonConflict) {
this.$notify({ type: "danger", text: "Cannot select same person as giver and recipient." });
return;
}
}
```
The validation logic implements several business rules before allowing gift submission. Users must have an active decentralized identifier selected, cannot submit negative amounts, and must provide either a description or a numeric amount. The system also prevents the logical error of selecting the same person as both giver and recipient in person-to-person transactions.
## Gift Recording Logic
### API Parameter Mapping
The component translates UI selections into endorser server API parameters:
```typescript
// Person-to-Person Gift
{
fromDid: giver.did,
toDid: receiver.did,
description: description,
amount: parseFloat(amountInput),
unit: unitCode
}
// Person-to-Project Gift
{
fromDid: giver.did,
fulfillsProjectHandleId: toProjectId,
description: description,
amount: parseFloat(amountInput),
unit: unitCode
}
// Project-to-Person Gift
{
providerPlanHandleId: fromProjectId,
toDid: receiver.did,
description: description,
amount: parseFloat(amountInput),
unit: unitCode
}
```
The final step in the gift recording process involves translating the user's selections into the specific parameter format expected by the endorser server API. This mapping is complex because the API uses different fields depending on whether entities are people or projects. For project-to-person gifts, the project is identified by its handle ID in the `providerPlanHandleId` field, while the person is identified in the `toDid` field. Person-to-project gifts reverse this pattern, using `fromDid` for the person and `fulfillsProjectHandleId` for the project. Person-to-person gifts use the simpler `fromDid` and `toDid` pattern.
## State Management
### Core State Variables
```typescript
// UI State
visible: boolean = false; // Dialog visibility
currentStep: number = 1; // Current step (1 or 2)
stepType: string = "giver"; // Current selection type
// Entity State
giver?: GiverReceiverInputInfo; // Selected giver
receiver?: GiverReceiverInputInfo; // Selected recipient
giverEntityType: "person" | "project";
recipientEntityType: "person" | "project";
// Form State
description: string = ""; // Gift description
amountInput: string = "0"; // Gift amount
unitCode: string = "HUR"; // Unit of measurement
// Data State
allContacts: Contact[] = []; // Available contacts
projects: PlanData[] = []; // Available projects
activeDid: string = ""; // Current user's DID
```
The component maintains several categories of state to track the user's progress through the gift recording process. UI state controls the dialog's visibility and current step, while entity state tracks the selected giver and recipient along with their types. Form state holds the user's input for the gift details, and data state contains the available options loaded from the database and API. This separation makes the component's behavior predictable and easier to debug.
### Navigation State Transitions
```mermaid
graph TD
A[Dialog Closed] --> B[open() called]
B --> C{Giver pre-selected?}
C -->|Yes| D[Step 2: Gift Details]
C -->|No| E[Step 1: Entity Selection]
E --> F{Entity selected}
F --> D
D --> G{User action}
G -->|Edit Entity| E
G -->|Confirm| H[Submit Gift]
G -->|Cancel| A
H --> I{Success?}
I -->|Yes| A
I -->|No| D
```
The state transition diagram illustrates the possible paths users can take through the dialog. The flow begins when the dialog is opened and branches based on whether sufficient context was provided to skip entity selection. Users can move back and forth between steps, edit their selections, or cancel at any time. After submitting a gift, the flow either returns to the closed state (on success) or back to the details step (on error) to allow for corrections.
## Error Handling
### Validation Layers
1. **UI Prevention:** Disable/gray out invalid options
2. **Form Validation:** Check required fields and business rules
3. **API Validation:** Handle server-side errors gracefully
4. **User Feedback:** Clear error messages with specific guidance
The component implements a multi-layered approach to error prevention and handling. The first layer prevents errors by disabling invalid options in the user interface, such as graying out contacts that would create conflicts. The second layer validates form inputs before submission, checking for required fields and business rule violations. The third layer handles errors returned by the server API, while the fourth layer ensures users receive clear, actionable feedback about any issues that occur.
### Common Error Scenarios
- **Missing Identifier:** User hasn't selected an active DID
- **Negative Amount:** Prevent negative gift values
- **Empty Gift:** Require either description or amount
- **Person Conflict:** Same person selected as giver and recipient
- **Network Errors:** API server unreachable or returns errors
- **Database Errors:** Local storage issues
Each error scenario is handled with specific validation logic and user-friendly error messages. The component provides clear guidance on how to resolve issues, such as directing users to select an identifier or adjust their amount input. Network and database errors are caught and presented with appropriate fallback options.
## Security Considerations
### Data Privacy
- **DID Protection:** User identifiers only shared with explicit consent
- **Local Storage:** Sensitive data encrypted in local database
- **Network Transport:** All API calls use HTTPS encryption
The component maintains strict privacy controls over user data, particularly decentralized identifiers which serve as the core of user identity in the system. DIDs are only transmitted to the server when users explicitly choose to record a gift, and all local storage uses encryption to protect sensitive information.
### Input Validation
- **SQL Injection Prevention:** Parameterized database queries
- **XSS Protection:** Input sanitization and Vue's built-in escaping
- **Amount Validation:** Numeric input validation and range checking
All user inputs are validated both on the client side for immediate feedback and on the server side for security. The component uses parameterized queries to prevent SQL injection attacks and relies on Vue's built-in template escaping to prevent cross-site scripting vulnerabilities.
## Performance Optimization
### Lazy Loading
- **Projects:** Only loaded when needed for project-related gifts
- **Contacts:** Cached after initial load to avoid repeated queries
- **Database:** Optimized queries with proper indexing
The component optimizes performance by loading data only when needed. Projects are fetched from the API only when the dialog needs to display them, reducing unnecessary network requests. Contact information is cached after the initial load to avoid repeated database queries. Database operations are optimized through proper indexing and efficient query patterns.
### Reactive Updates
- **Watchers:** Automatically update entity types when props change
- **Computed Properties:** Efficient conflict detection and UI state
- **Minimal Re-renders:** Strategic use of v-show vs v-if
Vue's reactivity system is leveraged to ensure the interface stays synchronized with changing data while minimizing unnecessary re-renders. Watchers automatically update internal state when component properties change, computed properties efficiently calculate derived state like conflict detection, and the template uses v-show instead of v-if where appropriate to avoid expensive DOM manipulations.
## Integration Points
### External Dependencies
- **Endorser Server:** Gift submission and verification
- **Database Layer:** Contact and settings management
- **Platform Services:** Cross-platform database access
- **Router:** Navigation to related views
The component integrates with several external systems to provide its functionality. The endorser server handles the cryptographic signing and publishing of gift records to the distributed network. The database layer manages local storage of contacts and user settings. Platform services provide an abstraction layer for database access across different deployment targets (web, mobile, desktop). The Vue router enables navigation to related views while preserving state.
### Child Components
- **EntityIcon:** Person avatar rendering with fallbacks
- **ProjectIcon:** Project icon rendering with custom images
- **Notification System:** User feedback and error reporting
The dialog relies on several child components to provide specialized functionality. EntityIcon handles the complex logic of rendering person avatars with appropriate fallbacks when images aren't available. ProjectIcon performs similar duties for project representations. The notification system provides consistent user feedback across the application for both success and error conditions.
## Future Considerations
### Extensibility
- **New Entity Types:** Architecture supports additional entity types
- **Custom Units:** Easy to add new currency/unit types
- **Workflow Steps:** Framework supports additional steps if needed
The component's architecture is designed to accommodate future enhancements without major restructuring. The entity type system could be extended to support new types beyond people and projects. The unit selection system can easily accommodate new currencies or measurement types. The step-based workflow could be expanded to include additional steps for more complex gift recording scenarios.
### Accessibility
- **Keyboard Navigation:** All interactive elements accessible
- **Screen Readers:** Semantic HTML and ARIA labels
- **Color Contrast:** Visual indicators don't rely solely on color
Accessibility considerations ensure the component is usable by people with disabilities. All interactive elements can be navigated using only the keyboard, semantic HTML and ARIA labels provide context for screen readers, and visual indicators use more than just color to convey information. These features make the gift recording process inclusive for all users.
### Testing Strategy
- **Unit Tests:** Individual method testing
- **Integration Tests:** Full workflow testing
- **E2E Tests:** User journey validation
- **Error Scenarios:** Comprehensive error path testing
A comprehensive testing strategy ensures the component works reliably across different scenarios and platforms. Unit tests validate individual methods and computed properties, integration tests verify the complete workflow from opening to submission, end-to-end tests simulate real user journeys, and error scenario tests ensure graceful handling of failure conditions.
## Component Usage and Integration Context
### Primary Views and Usage Patterns
The `GiftedDialog` component is referenced and used throughout the Time Safari application in various contexts, each with specific requirements and configurations. Understanding where and how this component is integrated provides insight into its versatility and the different user workflows it supports.
#### 1. HomeView - Main Entry Point
**File:** `src/views/HomeView.vue`
**Context:** Primary application dashboard and activity feed
The HomeView serves as the main entry point for gift recording, providing users with multiple pathways to create gift records. The dialog is integrated here with a `showProjectsDialog` property that controls whether project options are displayed in the entity selection interface.
```vue
```
**Usage Patterns:**
- **Gift Prompts Integration:** Connected with `GiftedPrompts` component to provide suggested gift ideas
- **"Unnamed" Giver Support:** Special handling for recording gifts from unknown or unnamed individuals
- **Context-Aware Opening:** Different title and prompt text based on whether recording from a person or project
The HomeView implementation includes sophisticated logic for handling "Unnamed" givers, where the dialog is opened with specific pre-selections and immediately advances to Step 2 after calling `selectGiver()`. This streamlines the user experience when users want to record gifts from people not in their contact list.
#### 2. ProjectViewView - Project-Specific Context
**File:** `src/views/ProjectViewView.vue`
**Context:** Individual project pages with bidirectional gift recording
ProjectViewView contains two separate instances of GiftedDialog to handle different gift directions:
```vue
```
**Specialized Features:**
- **Bidirectional Gift Recording:** Supports both giving to and receiving from projects
- **Project Context Locking:** The project entity is pre-selected and cannot be changed
- **Offer Integration:** Can be opened with pre-populated offer information
- **Project-Specific Prompts:** Custom titles and descriptions based on project context
The ProjectViewView demonstrates the component's flexibility in handling complex scenarios where one entity (the project) is fixed while the other (person) needs to be selected. The `isFromProjectView` property helps the dialog understand this constrained context.
#### 3. ContactsView - Contact-Specific Gift Recording
**File:** `src/views/ContactsView.vue`
**Context:** Contact management with direct gift recording capabilities
ContactsView integrates the dialog for recording gifts between the current user and specific contacts, with sophisticated confirmation flows to ensure users understand the direction of the gift:
```vue
```
**Key Features:**
- **Bidirectional Confirmation:** Users confirm whether they're recording giving or receiving
- **Contact Pre-selection:** The contact is already known, simplifying entity selection
- **Direction Clarity:** Clear messaging about gift direction to prevent confusion
The ContactsView implementation includes a two-step confirmation process (`confirmShowGiftedDialog` and `showGiftedDialog`) that ensures users clearly understand whether they're recording giving something to a contact or receiving something from them.
#### 4. ContactGiftingView - Specialized Gift Recording Interface
**File:** `src/views/ContactGiftingView.vue`
**Context:** Dedicated interface for complex gift recording scenarios
This view provides a specialized interface for gift recording with advanced entity type handling and step management:
```vue
```
**Advanced Features:**
- **Dynamic Entity Type Resolution:** Supports complex scenarios with projects and people
- **Step Type Management:** Handles both giver and recipient selection workflows
- **Context Preservation:** Maintains complex state across navigation
ContactGiftingView demonstrates the most sophisticated usage of GiftedDialog, with comprehensive support for all entity type combinations and step management scenarios.
#### 5. ClaimView - Offer Fulfillment Context
**File:** `src/views/ClaimView.vue`
**Context:** Converting offers into actual gift records
ClaimView uses GiftedDialog to record gifts that fulfill existing offers, creating a connection between the offer system and gift recording:
```vue
```
**Offer Integration Features:**
- **Offer Pre-population:** Gift details can be pre-filled from offer information
- **Fulfillment Tracking:** Links gifts to specific offers for tracking purposes
- **Context-Aware Prompts:** Custom messaging for offer fulfillment scenarios
#### 6. Supporting Views - Additional Integration Points
**RecentOffersToUserView & RecentOffersToUserProjectsView:**
- Provide quick access to gift recording from offer listings
- Support both personal and project-related offer contexts
**NewActivityView:**
- Integrates gift recording into activity creation workflows
- Supports creating gifts as part of broader activity documentation
### Integration Patterns and Best Practices
#### Common Integration Pattern
Most views follow a consistent pattern for integrating GiftedDialog:
1. **Template Declaration:** Include the component with appropriate props
2. **Reference Setup:** Use `ref` attribute for programmatic access
3. **Method Integration:** Implement `openDialog` or similar methods
4. **Context Configuration:** Set appropriate props based on view context
5. **State Management:** Handle dialog results and state updates
#### Props Configuration Strategy
The component's props are designed to work together to create specific behaviors:
- **Mutual Exclusivity:** `fromProjectId` and `toProjectId` are mutually exclusive
- **Context Indicators:** `isFromProjectView` provides navigation context
- **Display Control:** `showProjects` overrides default entity type logic
- **Behavioral Modification:** Props combination determines available workflows
#### Error Handling and User Experience
Each integration point implements appropriate error handling and user experience considerations:
- **Validation Feedback:** Clear error messages for invalid selections
- **Context Preservation:** State maintained across navigation
- **Graceful Degradation:** Fallback options when data is unavailable
- **Accessibility Support:** Keyboard navigation and screen reader compatibility
### Development and Maintenance Considerations
#### Component Coupling
The widespread usage of GiftedDialog creates important coupling considerations:
- **Interface Stability:** Changes to the component's API affect multiple views
- **Testing Requirements:** Each integration context requires specific test coverage
- **Performance Impact:** Component optimization benefits multiple user workflows
- **Documentation Needs:** Clear usage patterns help maintain consistency
#### Future Evolution
The component's integration patterns suggest several areas for future development:
- **Workflow Standardization:** Common patterns could be extracted into mixins
- **State Management:** Complex state could benefit from centralized management
- **Type Safety:** Better TypeScript integration for props and methods
- **Accessibility Enhancement:** Consistent accessibility patterns across integrations
This comprehensive integration context demonstrates how GiftedDialog serves as a central component in the Time Safari application's gift recording ecosystem, supporting diverse user workflows while maintaining consistency and usability across different contexts.
## Detailed Implementation Roadmap for Future Enhancements
### 1. New Entity Types Implementation
The current architecture supports people and projects, but could be extended to support additional entity types such as organizations, events, or resources. Here's precisely what these changes would look like:
#### Current Entity Type System
```typescript
// Current implementation in GiftedDialog.vue
giverEntityType: "person" | "project" = "person";
recipientEntityType: "person" | "project" = "person";
updateEntityTypes() {
this.giverEntityType = "person";
this.recipientEntityType = "person";
if (this.showProjects) {
this.giverEntityType = "project";
} else if (this.fromProjectId) {
this.giverEntityType = "project";
} else if (this.toProjectId) {
this.recipientEntityType = "project";
}
}
```
#### Extended Entity Type System
```typescript
// Enhanced implementation with new entity types
type EntityType = "person" | "project" | "organization" | "event" | "resource";
// New props to support additional entity types
@Prop() fromOrganizationId = "";
@Prop() toOrganizationId = "";
@Prop() fromEventId = "";
@Prop() toEventId = "";
@Prop() fromResourceId = "";
@Prop() toResourceId = "";
@Prop() showOrganizations = false;
@Prop() showEvents = false;
@Prop() showResources = false;
giverEntityType: EntityType = "person";
recipientEntityType: EntityType = "person";
updateEntityTypes() {
// Reset to default
this.giverEntityType = "person";
this.recipientEntityType = "person";
// Apply context-specific logic with priority order
if (this.showOrganizations) {
this.giverEntityType = "organization";
} else if (this.showEvents) {
this.giverEntityType = "event";
} else if (this.showResources) {
this.giverEntityType = "resource";
} else if (this.showProjects) {
this.giverEntityType = "project";
} else if (this.fromOrganizationId) {
this.giverEntityType = "organization";
} else if (this.fromEventId) {
this.giverEntityType = "event";
} else if (this.fromResourceId) {
this.giverEntityType = "resource";
} else if (this.fromProjectId) {
this.giverEntityType = "project";
} else if (this.toOrganizationId) {
this.recipientEntityType = "organization";
} else if (this.toEventId) {
this.recipientEntityType = "event";
} else if (this.toResourceId) {
this.recipientEntityType = "resource";
} else if (this.toProjectId) {
this.recipientEntityType = "project";
}
}
```
#### New Component Integration Required
```vue
```
#### API Parameter Mapping Extensions
```typescript
// Extended gift recording logic
async recordGive() {
let apiParams: any = {
description: this.description,
amount: parseFloat(this.amountInput),
unit: this.unitCode
};
// Entity-specific parameter mapping
switch (`${this.giverEntityType}-${this.recipientEntityType}`) {
case "person-person":
apiParams.fromDid = this.giver?.did;
apiParams.toDid = this.receiver?.did;
break;
case "person-project":
apiParams.fromDid = this.giver?.did;
apiParams.fulfillsProjectHandleId = this.toProjectId;
break;
case "project-person":
apiParams.providerPlanHandleId = this.fromProjectId;
apiParams.toDid = this.receiver?.did;
break;
case "person-organization":
apiParams.fromDid = this.giver?.did;
apiParams.fulfillsOrganizationHandleId = this.toOrganizationId;
break;
case "organization-person":
apiParams.providerOrganizationHandleId = this.fromOrganizationId;
apiParams.toDid = this.receiver?.did;
break;
case "person-event":
apiParams.fromDid = this.giver?.did;
apiParams.fulfillsEventHandleId = this.toEventId;
break;
case "event-person":
apiParams.providerEventHandleId = this.fromEventId;
apiParams.toDid = this.receiver?.did;
break;
case "person-resource":
apiParams.fromDid = this.giver?.did;
apiParams.fulfillsResourceHandleId = this.toResourceId;
break;
case "resource-person":
apiParams.providerResourceHandleId = this.fromResourceId;
apiParams.toDid = this.receiver?.did;
break;
default:
throw new Error(`Unsupported entity combination: ${this.giverEntityType}-${this.recipientEntityType}`);
}
// Submit to endorser server
const result = await createAndSubmitGive(apiParams);
}
```
### 2. Custom Units Implementation
The current unit system supports Hours, USD, BTC, BX, and ETH. Here's how to add new currency/unit types:
#### Current Unit System
```typescript
// Current implementation in GiftedDialog.vue
unitCode = "HUR";
// In template
```
#### Enhanced Unit System
```typescript
// New unit configuration system
interface UnitDefinition {
code: string;
name: string;
symbol: string;
category: "time" | "currency" | "token" | "custom";
decimals: number;
conversionRate?: number; // Optional conversion to base unit
}
// Configurable unit registry
const UNIT_REGISTRY: UnitDefinition[] = [
// Time units
{ code: "HUR", name: "Hours", symbol: "hr", category: "time", decimals: 2 },
{ code: "MIN", name: "Minutes", symbol: "min", category: "time", decimals: 0 },
{ code: "DAY", name: "Days", symbol: "day", category: "time", decimals: 1 },
// Traditional currencies
{ code: "USD", name: "US Dollar", symbol: "$", category: "currency", decimals: 2 },
{ code: "EUR", name: "Euro", symbol: "€", category: "currency", decimals: 2 },
{ code: "GBP", name: "British Pound", symbol: "£", category: "currency", decimals: 2 },
// Cryptocurrencies
{ code: "BTC", name: "Bitcoin", symbol: "₿", category: "token", decimals: 8 },
{ code: "ETH", name: "Ethereum", symbol: "Ξ", category: "token", decimals: 18 },
{ code: "BX", name: "BX Token", symbol: "BX", category: "token", decimals: 4 },
// Custom units
{ code: "KARMA", name: "Karma Points", symbol: "♦", category: "custom", decimals: 0 },
{ code: "SKILL", name: "Skill Points", symbol: "⚡", category: "custom", decimals: 1 },
{ code: "REP", name: "Reputation", symbol: "★", category: "custom", decimals: 0 }
];
// Enhanced component implementation
export default class GiftedDialog extends Vue {
unitCode = "HUR";
availableUnits: UnitDefinition[] = [];
async mounted() {
// Load available units based on user preferences or context
this.availableUnits = await this.loadAvailableUnits();
}
async loadAvailableUnits(): Promise {
// Load from user settings or default configuration
const userSettings = await this.getUserSettings();
if (userSettings.customUnits && userSettings.customUnits.length > 0) {
return [...UNIT_REGISTRY, ...userSettings.customUnits];
}
return UNIT_REGISTRY;
}
get unitsByCategory() {
return this.availableUnits.reduce((acc, unit) => {
if (!acc[unit.category]) acc[unit.category] = [];
acc[unit.category].push(unit);
return acc;
}, {} as Record);
}
get selectedUnit(): UnitDefinition | undefined {
return this.availableUnits.find(unit => unit.code === this.unitCode);
}
formatAmount(amount: number): string {
const unit = this.selectedUnit;
if (!unit) return amount.toString();
const formattedAmount = amount.toFixed(unit.decimals);
return `${unit.symbol}${formattedAmount}`;
}
}
```
#### Enhanced Template with Categorized Units
```vue
{{ formatAmount(parseFloat(amountInput) || 0) }}
```
### 3. Workflow Steps Enhancement
The current two-step workflow could be expanded to include additional steps for more complex scenarios:
#### Current Workflow Structure
```typescript
// Current step management
currentStep = 1; // 1 = Entity Selection, 2 = Gift Details
goToStep2() {
this.currentStep = 2;
}
goBackToStep1(stepType: string) {
this.stepType = stepType;
this.currentStep = 1;
}
```
#### Enhanced Multi-Step Workflow
```typescript
// Enhanced workflow with additional steps
type WorkflowStep = "entity-selection" | "gift-details" | "verification" | "attachment" | "confirmation";
interface WorkflowDefinition {
steps: WorkflowStep[];
currentStepIndex: number;
canSkipStep: (step: WorkflowStep) => boolean;
isStepRequired: (step: WorkflowStep) => boolean;
}
export default class GiftedDialog extends Vue {
workflow: WorkflowDefinition = {
steps: ["entity-selection", "gift-details", "verification", "confirmation"],
currentStepIndex: 0,
canSkipStep: (step) => {
switch (step) {
case "entity-selection":
return this.giver && this.receiver; // Skip if entities pre-selected
case "attachment":
return true; // Always optional
case "verification":
return parseFloat(this.amountInput) === 0; // Skip for zero-amount gifts
default:
return false;
}
},
isStepRequired: (step) => {
switch (step) {
case "entity-selection":
case "gift-details":
case "confirmation":
return true;
case "verification":
return parseFloat(this.amountInput) > 0;
case "attachment":
return false;
default:
return false;
}
}
};
get currentStep(): WorkflowStep {
return this.workflow.steps[this.workflow.currentStepIndex];
}
get canGoNext(): boolean {
return this.workflow.currentStepIndex < this.workflow.steps.length - 1;
}
get canGoPrevious(): boolean {
return this.workflow.currentStepIndex > 0;
}
async nextStep() {
if (!this.canGoNext) return;
// Validate current step before proceeding
const isValid = await this.validateCurrentStep();
if (!isValid) return;
this.workflow.currentStepIndex++;
// Skip optional steps if conditions are met
while (
this.workflow.currentStepIndex < this.workflow.steps.length &&
this.workflow.canSkipStep(this.currentStep)
) {
this.workflow.currentStepIndex++;
}
}
previousStep() {
if (!this.canGoPrevious) return;
this.workflow.currentStepIndex--;
// Skip back over optional steps
while (
this.workflow.currentStepIndex >= 0 &&
this.workflow.canSkipStep(this.currentStep)
) {
this.workflow.currentStepIndex--;
}
}
async validateCurrentStep(): Promise {
switch (this.currentStep) {
case "entity-selection":
return this.validateEntitySelection();
case "gift-details":
return this.validateGiftDetails();
case "verification":
return this.validateVerification();
case "attachment":
return true; // Always valid since optional
case "confirmation":
return this.validateConfirmation();
default:
return false;
}
}
private validateEntitySelection(): boolean {
if (!this.giver || !this.receiver) {
this.$notify({
type: "danger",
text: "Please select both giver and recipient"
});
return false;
}
return true;
}
private validateGiftDetails(): boolean {
if (!this.description && !parseFloat(this.amountInput)) {
this.$notify({
type: "danger",
text: "Please provide either a description or amount"
});
return false;
}
return true;
}
private validateVerification(): boolean {
// Implement verification logic (e.g., confirm large amounts)
const amount = parseFloat(this.amountInput);
if (amount > 1000) {
// Could show a confirmation dialog here
return this.confirmLargeAmount();
}
return true;
}
private validateConfirmation(): boolean {
// Final validation before submission
return this.validateEntitySelection() && this.validateGiftDetails();
}
}
```
#### Enhanced Template with Multi-Step Navigation
```vue
{{ step.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase()) }}
Verify Gift Details
{{ giver?.name || 'Unnamed' }}
{{ receiver?.name || 'Unnamed' }}
{{ formatAmount(parseFloat(amountInput) || 0) }}
{{ description || 'No description' }}
Large Amount Detected
Please verify this {{ formatAmount(parseFloat(amountInput)) }} gift is correct.
Add Attachments (Optional)
Attached Files:
{{ attachment.name }}
Confirm Gift
{{ formatAmount(parseFloat(amountInput) || 0) }}
{{ description || 'Gift' }}
From {{ giver?.name || 'Unnamed' }} to {{ receiver?.name || 'Unnamed' }}
This gift will be recorded on the distributed network and cannot be deleted.
```
### 4. Workflow Standardization Through Mixins
To address the repetitive integration patterns across views, common functionality could be extracted into mixins:
#### Current Repetitive Pattern
```typescript
// Repeated across HomeView, ContactsView, ProjectViewView, etc.
openDialog(giver?: GiverReceiverInputInfo, description?: string) {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{ did: this.activeDid, name: "You" },
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
}
```
#### Standardized Mixin Implementation
```typescript
// New file: src/mixins/GiftedDialogMixin.ts
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { GiverReceiverInputInfo } from "@/libs/util";
interface GiftedDialogConfig {
ref: string;
defaultGiver?: GiverReceiverInputInfo;
defaultReceiver?: GiverReceiverInputInfo;
titleTemplate?: string;
promptTemplate?: string;
fromProjectId?: string;
toProjectId?: string;
showProjects?: boolean;
isFromProjectView?: boolean;
}
@Component
export default class GiftedDialogMixin extends Vue {
protected giftedDialogConfig: GiftedDialogConfig = {
ref: "customDialog"
};
/**
* Standardized method to open GiftedDialog with consistent parameter handling
*/
protected openGiftedDialog(options: {
giver?: GiverReceiverInputInfo | "Unnamed";
receiver?: GiverReceiverInputInfo;
offerId?: string;
customTitle?: string;
prompt?: string;
callbackOnSuccess?: (amount: number) => void;
} = {}) {
const config = this.giftedDialogConfig;
const dialog = this.$refs[config.ref] as GiftedDialog;
if (!dialog) {
console.error(`GiftedDialog ref '${config.ref}' not found`);
return;
}
// Handle "Unnamed" giver special case
if (options.giver === "Unnamed") {
dialog.open(
undefined,
options.receiver || config.defaultReceiver || this.getCurrentUser(),
options.offerId,
options.customTitle || "Given by Unnamed",
options.prompt
);
dialog.selectGiver(); // Immediately advance to step 2
return;
}
// Standard dialog opening
const giver = options.giver || config.defaultGiver;
const receiver = options.receiver || config.defaultReceiver || this.getCurrentUser();
const title = options.customTitle || this.generateTitle(giver, receiver);
dialog.open(
giver,
receiver,
options.offerId,
title,
options.prompt,
options.callbackOnSuccess
);
}
/**
* Generate contextual title based on entities
*/
private generateTitle(
giver?: GiverReceiverInputInfo,
receiver?: GiverReceiverInputInfo
): string {
const template = this.giftedDialogConfig.titleTemplate;
if (template) {
return template
.replace("{giver}", giver?.name || "someone")
.replace("{receiver}", receiver?.name || "someone");
}
if (giver) {
return `Given by ${giver.name || "someone not named"}`;
}
return "Record a Gift";
}
/**
* Get current user as GiverReceiverInputInfo
*/
private getCurrentUser(): GiverReceiverInputInfo {
return {
did: this.activeDid || "",
name: "You"
};
}
/**
* Handle dialog success callback
*/
protected onGiftRecorded(amount: number) {
this.$notify({
group: "alert",
type: "success",
title: "Gift Recorded",
text: `Successfully recorded gift of ${amount}`
});
}
/**
* Utility method for project-specific dialog opening
*/
protected openProjectGiftDialog(
projectId: string,
isGiving: boolean,
customTitle?: string
) {
const projectEntity = this.getProjectEntity(projectId);
if (isGiving) {
// User is giving to project
this.openGiftedDialog({
giver: this.getCurrentUser(),
receiver: projectEntity,
customTitle: customTitle || `Gift to ${projectEntity?.name || "Project"}`
});
} else {
// User received from project
this.openGiftedDialog({
giver: projectEntity,
receiver: this.getCurrentUser(),
customTitle: customTitle || `Gift from ${projectEntity?.name || "Project"}`
});
}
}
/**
* Get project entity by ID (to be implemented by consuming component)
*/
protected getProjectEntity(projectId: string): GiverReceiverInputInfo | undefined {
// Override in consuming component
return undefined;
}
// Abstract properties that consuming components should provide
protected abstract activeDid: string;
}
```
#### Updated Component Implementation Using Mixin
```typescript
// Updated HomeView.vue using the mixin
import { Component, Mixins } from "vue-facing-decorator";
import GiftedDialogMixin from "@/mixins/GiftedDialogMixin";
@Component({
components: {
GiftedDialog,
// ... other components
}
})
export default class HomeView extends Mixins(GiftedDialogMixin) {
// Configure the mixin
protected giftedDialogConfig = {
ref: "customDialog",
titleTemplate: "Given by {giver}",
showProjects: this.showProjectsDialog
};
// Simplified dialog opening methods
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
this.openGiftedDialog({
giver,
prompt: description,
callbackOnSuccess: this.onGiftRecorded
});
}
openGiftedPrompts() {
(this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) =>
this.openDialog(giver as GiverReceiverInputInfo, description)
);
}
// Override abstract method
protected getProjectEntity(projectId: string): GiverReceiverInputInfo | undefined {
const project = this.projects.find(p => p.handleId === projectId);
if (!project) return undefined;
return {
did: project.handleId,
name: project.name,
handleId: project.handleId,
image: project.image
};
}
}
```
### 5. State Management Centralization
Complex state could be moved to a centralized Pinia store:
#### New Pinia Store Implementation
```typescript
// New file: src/stores/giftDialog.ts
import { defineStore } from 'pinia';
import { GiverReceiverInputInfo } from '@/libs/util';
interface GiftDialogState {
isVisible: boolean;
currentStep: WorkflowStep;
giverEntityType: EntityType;
recipientEntityType: EntityType;
giver?: GiverReceiverInputInfo;
receiver?: GiverReceiverInputInfo;
description: string;
amountInput: string;
unitCode: string;
validationErrors: string[];
isSubmitting: boolean;
}
export const useGiftDialogStore = defineStore('giftDialog', {
state: (): GiftDialogState => ({
isVisible: false,
currentStep: 'entity-selection',
giverEntityType: 'person',
recipientEntityType: 'person',
giver: undefined,
receiver: undefined,
description: '',
amountInput: '0',
unitCode: 'HUR',
validationErrors: [],
isSubmitting: false
}),
getters: {
isValid: (state) => {
return state.giver && state.receiver &&
(state.description || parseFloat(state.amountInput) > 0);
},
hasPersonConflict: (state) => {
return state.giver?.did === state.receiver?.did &&
state.giver?.did !== undefined &&
state.giverEntityType === 'person' &&
state.recipientEntityType === 'person';
},
formattedAmount: (state) => {
const amount = parseFloat(state.amountInput) || 0;
const unit = UNIT_REGISTRY.find(u => u.code === state.unitCode);
return unit ? `${unit.symbol}${amount.toFixed(unit.decimals)}` : amount.toString();
}
},
actions: {
openDialog(config: {
giver?: GiverReceiverInputInfo;
receiver?: GiverReceiverInputInfo;
prompt?: string;
}) {
this.isVisible = true;
this.giver = config.giver;
this.receiver = config.receiver;
this.description = config.prompt || '';
this.currentStep = config.giver ? 'gift-details' : 'entity-selection';
this.resetValidation();
},
closeDialog() {
this.isVisible = false;
this.resetForm();
},
setGiver(giver: GiverReceiverInputInfo) {
this.giver = giver;
this.validateStep();
},
setReceiver(receiver: GiverReceiverInputInfo) {
this.receiver = receiver;
this.validateStep();
},
updateDescription(description: string) {
this.description = description;
this.validateStep();
},
updateAmount(amount: string) {
this.amountInput = amount;
this.validateStep();
},
updateUnit(unitCode: string) {
this.unitCode = unitCode;
},
addAttachment(file: File) {
this.attachments.push(file);
},
removeAttachment(index: number) {
this.attachments.splice(index, 1);
},
validateStep(): boolean {
this.validationErrors = [];
switch (this.currentStep) {
case 'entity-selection':
if (!this.giver) this.validationErrors.push('Please select a giver');
if (!this.receiver) this.validationErrors.push('Please select a receiver');
if (this.hasPersonConflict) {
this.validationErrors.push('Cannot select the same person as both giver and recipient');
}
break;
case 'gift-details':
if (!this.description && !parseFloat(this.amountInput)) {
this.validationErrors.push('Please provide description or amount');
}
if (parseFloat(this.amountInput) < 0) {
this.validationErrors.push('Amount cannot be negative');
}
break;
}
return this.validationErrors.length === 0;
},
async submitGift(): Promise {
if (!this.isValid) return false;
this.isSubmitting = true;
try {
// Submit logic here
const result = await this.createGiftRecord();
if (result.success) {
this.closeDialog();
return true;
}
return false;
} finally {
this.isSubmitting = false;
}
},
private resetForm() {
this.giver = undefined;
this.receiver = undefined;
this.description = '';
this.amountInput = '0';
this.unitCode = 'HUR';
this.attachments = [];
this.resetValidation();
},
private resetValidation() {
this.validationErrors = [];
},
private async createGiftRecord() {
// Implementation would go here
return { success: true };
}
}
});
```
#### Updated Component Using Store
```typescript
// Updated GiftedDialog.vue using Pinia store
import { useGiftDialogStore } from '@/stores/giftDialog';
@Component({
components: { EntityIcon, ProjectIcon }
})
export default class GiftedDialog extends Vue {
private giftDialogStore = useGiftDialogStore();
// Computed properties map to store getters
get visible() { return this.giftDialogStore.isVisible; }
get currentStep() { return this.giftDialogStore.currentStep; }
get giver() { return this.giftDialogStore.giver; }
get receiver() { return this.giftDialogStore.receiver; }
get description() { return this.giftDialogStore.description; }
get amountInput() { return this.giftDialogStore.amountInput; }
get unitCode() { return this.giftDialogStore.unitCode; }
get validationErrors() { return this.giftDialogStore.validationErrors; }
get isSubmitting() { return this.giftDialogStore.isSubmitting; }
// Methods delegate to store actions
open(giver?: GiverReceiverInputInfo, receiver?: GiverReceiverInputInfo, prompt?: string) {
this.giftDialogStore.openDialog({ giver, receiver, prompt });
}
close() {
this.giftDialogStore.closeDialog();
}
selectGiver(giver: GiverReceiverInputInfo) {
this.giftDialogStore.setGiver(giver);
}
selectReceiver(receiver: GiverReceiverInputInfo) {
this.giftDialogStore.setReceiver(receiver);
}
async confirm() {
const success = await this.giftDialogStore.submitGift();
if (success) {
this.$notify({
type: "success",
text: "Gift recorded successfully"
});
}
}
}
```
These detailed implementations show precisely what the future enhancements would look like, providing clear roadmaps for extending the GiftedDialog component's capabilities while maintaining backward compatibility and improving maintainability.
---
## ANNEX: Complete GiftedDialog Component Inventory
### Purpose
This inventory serves as a comprehensive checklist to ensure no functionality is lost during the Pinia store refactoring. Each item must be accounted for in the new store-based implementation.
### A. Component Props
| Prop | Type | Default | Status | Store Field | Notes |
|------|------|---------|--------|-------------|-------|
| `fromProjectId` | string | `""` | ✅ Covered | N/A (passed as context) | Used to determine giverEntityType |
| `toProjectId` | string | `""` | ✅ Covered | N/A (passed as context) | Used to determine recipientEntityType |
| `showProjects` | boolean | `false` | ✅ Covered | N/A (passed as context) | Controls entity type display |
| `isFromProjectView` | boolean | `false` | ✅ Covered | N/A (passed as context) | Navigation context flag |
### B. Core State Variables
| Variable | Type | Default | Status | Store Field | Critical |
|----------|------|---------|--------|-------------|----------|
| `visible` | boolean | `false` | ✅ Covered | `isVisible` | Yes |
| `currentStep` | number | `1` | ✅ Covered | `currentStep` | Yes |
| `stepType` | string | `"giver"` | ✅ Covered | `entitySelectionMode` | Yes (renamed) |
| `giverEntityType` | EntityType | `"person"` | ✅ Covered | `giverEntityType` | Yes |
| `recipientEntityType` | EntityType | `"person"` | ✅ Covered | `recipientEntityType` | Yes |
| `giver` | GiverReceiverInputInfo? | `undefined` | ✅ Covered | `giver` | Yes |
| `receiver` | GiverReceiverInputInfo? | `undefined` | ✅ Covered | `receiver` | Yes |
| `description` | string | `""` | ✅ Covered | `description` | Yes |
| `amountInput` | string | `"0"` | ✅ Covered | `amountInput` | Yes |
| `unitCode` | string | `"HUR"` | ✅ Covered | `unitCode` | Yes |
### C. Context State Variables (CRITICAL - Previously Missing)
| Variable | Type | Default | Status | Store Field | Critical |
|----------|------|---------|--------|-------------|----------|
| `callbackOnSuccess` | Function? | `() => {}` | ❌ **ADDED** | `callbackOnSuccess` | **YES** |
| `customTitle` | string? | `undefined` | ❌ **ADDED** | `customTitle` | **YES** |
| `offerId` | string | `""` | ❌ **ADDED** | `offerId` | **YES** |
| `prompt` | string | `""` | ❌ **ADDED** | `prompt` | **YES** |
### D. API and Account State Variables (CRITICAL - Previously Missing)
| Variable | Type | Default | Status | Store Field | Critical |
|----------|------|---------|--------|-------------|----------|
| `apiServer` | string | `""` | ❌ **ADDED** | `apiServer` | **YES** |
| `activeDid` | string | `""` | ❌ **ADDED** | `activeDid` | **YES** |
| `allMyDids` | string[] | `[]` | ❌ **ADDED** | `allMyDids` | **YES** |
### E. Data State Variables
| Variable | Type | Default | Status | Store Field | Critical |
|----------|------|---------|--------|-------------|----------|
| `allContacts` | Contact[] | `[]` | ✅ Covered | `allContacts` | Yes |
| `projects` | PlanData[] | `[]` | ✅ Covered | `projects` | Yes |
### F. Utility References (CRITICAL - Previously Missing)
| Variable | Type | Status | Store Implementation | Critical |
|----------|------|--------|---------------------|----------|
| `libsUtil` | object | ❌ **NEEDS IMPORT** | Import in store | **YES** |
| `didInfo` | function | ❌ **NEEDS IMPORT** | Import in store | **YES** |
### G. Computed Properties
| Property | Return Type | Status | Store Getter | Critical |
|----------|-------------|--------|--------------|----------|
| `shouldShowProjects` | boolean | ✅ Covered | `shouldShowProjects` | Yes |
| `hasPersonConflict` | boolean | ✅ Covered | `hasPersonConflict` | Yes |
| `giftedDetailsQuery` | object | ✅ Covered | `giftedDetailsQuery` | Yes |
### H. Watchers
| Watched Property | Handler | Status | Store Implementation | Critical |
|------------------|---------|--------|---------------------|----------|
| `showProjects` | `onShowProjectsChange()` | ✅ Covered | Reactive in store | Yes |
| `fromProjectId` | `onFromProjectIdChange()` | ✅ Covered | Reactive in store | Yes |
| `toProjectId` | `onToProjectIdChange()` | ✅ Covered | Reactive in store | Yes |
### I. Public Methods (API Contract)
| Method | Parameters | Return Type | Status | Store Action | Critical |
|--------|------------|-------------|--------|--------------|----------|
| `open()` | giver?, receiver?, offerId?, customTitle?, prompt?, callback? | Promise\ | ✅ Covered | `openDialog()` | **YES** |
| `close()` | none | void | ✅ Covered | `closeDialog()` | Yes |
| `cancel()` | none | void | ✅ Covered | `cancel()` | Yes |
| `confirm()` | none | Promise\ | ✅ Covered | `submitGift()` | **YES** |
### J. Entity Selection Methods
| Method | Parameters | Return Type | Status | Store Action | Critical |
|--------|------------|-------------|--------|--------------|----------|
| `selectGiver()` | contact? | void | ✅ Covered | `setGiver()` | **YES** |
| `selectRecipient()` | contact? | void | ✅ Covered | `setReceiver()` | **YES** |
| `selectProject()` | project | void | ✅ Covered | `selectProject()` | **YES** |
| `selectRecipientProject()` | project | void | ✅ Covered | `selectRecipientProject()` | **YES** |
### K. Navigation Methods
| Method | Parameters | Return Type | Status | Store Action | Critical |
|--------|------------|-------------|--------|--------------|----------|
| `goBackToStep1()` | step | void | ✅ Covered | `setEntitySelectionMode()` + `setCurrentStep()` | **YES** |
### L. Utility Methods
| Method | Parameters | Return Type | Status | Store Action | Critical |
|--------|------------|-------------|--------|--------------|----------|
| `wouldCreateConflict()` | contactDid | boolean | ✅ Covered | `wouldCreateConflict()` | **YES** |
| `increment()` | none | void | ✅ Covered | `increment()` | Yes |
| `decrement()` | none | void | ✅ Covered | `decrement()` | Yes |
| `changeUnitCode()` | none | void | ✅ Covered | `changeUnitCode()` | Yes |
| `explainData()` | none | void | ✅ Covered | Component method | No |
### M. Business Logic Methods
| Method | Parameters | Return Type | Status | Store Action | Critical |
|--------|------------|-------------|--------|--------------|----------|
| `recordGive()` | giverDid, recipientDid, description, amount, unitCode | Promise\ | ✅ Covered | `submitGift()` | **YES** |
| `updateEntityTypes()` | none | void | ✅ Covered | Reactive logic | **YES** |
| `loadProjects()` | none | Promise\ | ✅ Covered | `loadProjects()` | **YES** |
### N. Data Loading Methods (CRITICAL - Previously Missing)
| Method | Parameters | Return Type | Status | Store Action | Critical |
|--------|------------|-------------|--------|--------------|----------|
| `loadAccountData()` | none | Promise\ | ❌ **ADDED** | `loadAccountData()` | **YES** |
| `loadContacts()` | none | Promise\ | ❌ **ADDED** | `loadContacts()` | **YES** |
### O. Helper Methods
| Method | Parameters | Return Type | Status | Implementation | Critical |
|--------|------------|-------------|--------|----------------|----------|
| `getGiveCreationErrorMessage()` | result | string | ✅ Covered | Component method | No |
| `eraseValues()` | none | void | ✅ Covered | `resetForm()` | Yes |
### P. Template Dependencies
| Template Reference | Type | Status | Store Implementation | Critical |
|-------------------|------|--------|---------------------|----------|
| `stepType` | string | ✅ Covered | `entitySelectionMode` | **YES** |
| `shouldShowProjects` | computed | ✅ Covered | `shouldShowProjects` getter | **YES** |
| `hasPersonConflict` | computed | ✅ Covered | `hasPersonConflict` getter | **YES** |
| `giftedDetailsQuery` | computed | ✅ Covered | `giftedDetailsQuery` getter | **YES** |
### Q. External Dependencies (Must be imported in store)
| Dependency | Source | Status | Required For | Critical |
|------------|--------|--------|--------------|----------|
| `createAndSubmitGive` | `libs/endorserServer` | ❌ **NEEDS IMPORT** | Gift submission | **YES** |
| `didInfo` | `libs/endorserServer` | ❌ **NEEDS IMPORT** | Contact display | **YES** |
| `getHeaders` | `libs/endorserServer` | ❌ **NEEDS IMPORT** | API calls | **YES** |
| `retrieveSettingsForActiveAccount` | `db/index` | ❌ **NEEDS IMPORT** | Account data | **YES** |
| `retrieveAccountDids` | `libs/util` | ❌ **NEEDS IMPORT** | DID management | **YES** |
| `PlatformServiceFactory` | `services/PlatformServiceFactory` | ❌ **NEEDS IMPORT** | Database access | **YES** |
| `databaseUtil` | `db/databaseUtil` | ❌ **NEEDS IMPORT** | Data mapping | **YES** |
| `logger` | `utils/logger` | ❌ **NEEDS IMPORT** | Error logging | **YES** |
| `UNIT_SHORT` | `libs/util` | ❌ **NEEDS IMPORT** | Unit display | **YES** |
| `PRIVACY_MESSAGE` | `libs/util` | ❌ **NEEDS IMPORT** | Data explanation | No |
### R. Type Definitions Required
| Type | Source | Status | Required For | Critical |
|------|--------|--------|--------------|----------|
| `GiverReceiverInputInfo` | `libs/util` | ✅ Covered | Entity info | **YES** |
| `Contact` | `db/tables/contacts` | ✅ Covered | Contact data | **YES** |
| `PlanData` | `interfaces/records` | ✅ Covered | Project data | **YES** |
| `NotificationIface` | `constants/app` | ✅ Covered | Notifications | Yes |
### S. Integration Points (Must be preserved)
| Integration | Views Using | Status | Refactoring Impact | Critical |
|-------------|-------------|--------|-------------------|----------|
| HomeView | Main entry | ✅ Planned | Phase 3.1 | **YES** |
| ProjectViewView | Dual dialogs | ✅ Planned | Phase 3.3 | **YES** |
| ContactsView | Contact gifts | ✅ Planned | Phase 3.2 | **YES** |
| ContactGiftingView | Advanced UI | ✅ Planned | Phase 3.4 | **YES** |
| ClaimView | Offer fulfillment | ✅ Planned | Phase 3.4 | **YES** |
| GiftedDetailsView | Advanced options | ✅ Covered | Query params | **YES** |
### T. Risk Assessment Summary
| Category | Items | Covered | Missing | Risk Level | Pre-Phase Status |
|----------|-------|---------|---------|------------|------------------|
| **Props** | 4 | 4 | 0 | ✅ **LOW** | ✅ Ready |
| **Core State** | 10 | 10 | 0 | ✅ **LOW** | ✅ Ready |
| **Context State** | 4 | 0 | 4 | 🚨 **HIGH** → ⏳ **IN PROGRESS** | 🔄 Pre-Phase 1.1 |
| **API/Account State** | 3 | 0 | 3 | 🚨 **HIGH** → ⏳ **IN PROGRESS** | 🔄 Pre-Phase 1.1 |
| **Data State** | 2 | 2 | 0 | ✅ **LOW** | ✅ Ready |
| **Methods** | 20 | 18 | 2 | ⚠️ **MEDIUM** → ⏳ **IN PROGRESS** | 🔄 Pre-Phase 1.3 |
| **External Deps** | 11 | 0 | 11 | 🚨 **HIGH** → ⏳ **IN PROGRESS** | 🔄 Pre-Phase 1.2 |
| **Integrations** | 6 | 6 | 0 | ✅ **LOW** | ✅ Ready |
### U. Action Items for Refactoring Plan
#### 🔄 **PRE-PHASE 1 (IN PROGRESS - 3 days)**
- [ ] **Pre-Phase 1.1**: Complete store interface with all missing fields
- [ ] **Pre-Phase 1.2**: Validate all external dependency imports
- [ ] **Pre-Phase 1.3**: Implement critical method stubs
- [ ] **Pre-Phase 1.4**: Run integration tests
- [ ] **Pre-Phase 1.5**: Update documentation and verify readiness
#### ⏸️ **CRITICAL (Must Complete Before Phase 1)**
- [ ] ✅ All context state fields added to store interface
- [ ] ✅ All API/account state fields added to store interface
- [ ] ✅ All external dependencies imported and tested in store
- [ ] ✅ Data loading actions implemented and tested in store
- [ ] ✅ Utility reference imports validated
#### ⚠️ **HIGH PRIORITY (Phase 1)**
- [ ] Implement complete store with all identified fields
- [ ] Create comprehensive test coverage for missing functionality
- [ ] Validate callback function handling
- [ ] Test offer ID integration
#### ✅ **MEDIUM PRIORITY (Phase 2-3)**
- [ ] Implement hybrid component with ALL computed properties
- [ ] Migrate views with full integration testing
- [ ] Validate query parameter generation
**REFACTORING READINESS:** ⏳ **PRE-PHASE 1 IN PROGRESS** - Critical fields being addressed
**ESTIMATED COMPLETION:** 17 days total (3 days pre-phase + 14 days original plan)
---
## PRE-PHASE 1: CRITICAL MISSING FIELDS RESOLUTION
### Overview
Before beginning the Pinia store refactoring, all critical missing fields must be addressed to prevent breaking changes. This pre-phase ensures 100% functionality preservation.
### Pre-Phase 1.1: Store Interface Completion
**Duration:** 1 day
**Files to create/modify:**
```
src/types/giftDialog.ts (complete interface)
src/stores/giftDialog.ts (skeleton with imports)
```
#### ✅ **TASK CHECKLIST:**
**1. Complete Type Definitions**
- [ ] Add all missing context state types
- [ ] Add all missing API/account state types
- [ ] Import all required external types
- [ ] Define complete callback function signatures
**Enhanced Type Definitions:**
```typescript
// src/types/giftDialog.ts
import { Contact } from '@/db/tables/contacts';
import { PlanData } from '@/interfaces/records';
import { GiverReceiverInputInfo } from '@/libs/util';
export type EntitySelectionMode = "giver" | "recipient";
export type EntityType = "person" | "project";
export type WorkflowStep = "entity-selection" | "gift-details" | "verification" | "confirmation";
export const ENTITY_SELECTION_MODE = {
GIVER: "giver" as const,
RECIPIENT: "recipient" as const
} as const;
// COMPLETE interface with ALL fields identified in audit
export interface GiftDialogState {
// UI State
isVisible: boolean;
currentStep: WorkflowStep;
entitySelectionMode: EntitySelectionMode;
// Entity State
giverEntityType: EntityType;
recipientEntityType: EntityType;
giver?: GiverReceiverInputInfo;
receiver?: GiverReceiverInputInfo;
// Form State
description: string;
amountInput: string;
unitCode: string;
// Context State (CRITICAL ADDITIONS)
callbackOnSuccess?: (amount: number) => void;
customTitle?: string;
offerId: string;
prompt: string;
// API and Account State (CRITICAL ADDITIONS)
apiServer: string;
activeDid: string;
allMyDids: string[];
// Data State
allContacts: Contact[];
projects: PlanData[];
// Validation State
validationErrors: string[];
isSubmitting: boolean;
}
// Callback function type for success handling
export type GiftSuccessCallback = (amount: number) => void;
// Configuration interface for opening dialog
export interface GiftDialogOpenConfig {
giver?: GiverReceiverInputInfo;
receiver?: GiverReceiverInputInfo;
offerId?: string;
customTitle?: string;
prompt?: string;
callbackOnSuccess?: GiftSuccessCallback;
}
// Query parameters interface for GiftedDetailsView
export interface GiftedDetailsQuery {
amountInput: string;
description: string;
giverDid?: string;
giverName?: string;
offerId: string;
fulfillsProjectId?: string;
providerProjectId?: string;
recipientDid?: string;
recipientName?: string;
unitCode: string;
}
```
**2. Import All External Dependencies**
- [ ] Create import statements for all 11 external dependencies
- [ ] Verify import paths are correct
- [ ] Test imports compile without errors
**Complete Import List:**
```typescript
// src/stores/giftDialog.ts
import { defineStore } from 'pinia';
import { Contact } from '@/db/tables/contacts';
import { PlanData } from '@/interfaces/records';
import {
GiverReceiverInputInfo,
retrieveAccountDids,
UNIT_SHORT,
PRIVACY_MESSAGE
} from '@/libs/util';
import {
createAndSubmitGive,
didInfo,
getHeaders,
serverMessageForUser
} from '@/libs/endorserServer';
import {
db,
retrieveSettingsForActiveAccount
} from '@/db/index';
import * as databaseUtil from '@/db/databaseUtil';
import { PlatformServiceFactory } from '@/services/PlatformServiceFactory';
import { logger } from '@/utils/logger';
import {
GiftDialogState,
EntitySelectionMode,
EntityType,
WorkflowStep,
GiftDialogOpenConfig,
GiftedDetailsQuery,
ENTITY_SELECTION_MODE
} from '@/types/giftDialog';
```
**3. Create Store Skeleton**
- [ ] Define complete state with default values
- [ ] Create placeholder actions for all methods
- [ ] Add validation for critical missing functionality
**Store Skeleton:**
```typescript
export const useGiftDialogStore = defineStore('giftDialog', {
state: (): GiftDialogState => ({
// UI State
isVisible: false,
currentStep: 'entity-selection',
entitySelectionMode: ENTITY_SELECTION_MODE.GIVER,
// Entity State
giverEntityType: 'person',
recipientEntityType: 'person',
giver: undefined,
receiver: undefined,
// Form State
description: '',
amountInput: '0',
unitCode: 'HUR',
// Context State (CRITICAL ADDITIONS)
callbackOnSuccess: undefined,
customTitle: undefined,
offerId: '',
prompt: '',
// API and Account State (CRITICAL ADDITIONS)
apiServer: '',
activeDid: '',
allMyDids: [],
// Data State
allContacts: [],
projects: [],
// Validation State
validationErrors: [],
isSubmitting: false
}),
getters: {
// All getters will be implemented in Phase 1.2
},
actions: {
// All actions will be implemented in Phase 1.2
}
});
```
### Pre-Phase 1.2: Dependency Validation
**Duration:** 0.5 days
**Objective:** Ensure all external dependencies are available and working
#### ✅ **TASK CHECKLIST:**
**1. Validate Import Paths**
- [ ] Test each import statement individually
- [ ] Verify all functions/classes are exported correctly
- [ ] Check for any missing dependencies in package.json
**2. Create Import Test File**
```typescript
// test-imports.ts (temporary file for validation)
import { createAndSubmitGive, didInfo, getHeaders } from '@/libs/endorserServer';
import { retrieveSettingsForActiveAccount } from '@/db/index';
import { retrieveAccountDids, UNIT_SHORT } from '@/libs/util';
import { PlatformServiceFactory } from '@/services/PlatformServiceFactory';
import * as databaseUtil from '@/db/databaseUtil';
import { logger } from '@/utils/logger';
// Test that all imports are available
console.log('Testing imports...');
console.log('createAndSubmitGive:', typeof createAndSubmitGive);
console.log('didInfo:', typeof didInfo);
console.log('getHeaders:', typeof getHeaders);
console.log('retrieveSettingsForActiveAccount:', typeof retrieveSettingsForActiveAccount);
console.log('retrieveAccountDids:', typeof retrieveAccountDids);
console.log('UNIT_SHORT:', typeof UNIT_SHORT);
console.log('PlatformServiceFactory:', typeof PlatformServiceFactory);
console.log('databaseUtil:', typeof databaseUtil);
console.log('logger:', typeof logger);
console.log('All imports successful!');
```
**3. Function Signature Validation**
- [ ] Verify `createAndSubmitGive` signature matches usage in component
- [ ] Check `didInfo` function parameters
- [ ] Validate `retrieveAccountDids` return type
- [ ] Confirm `PlatformServiceFactory.getInstance()` method exists
### Pre-Phase 1.3: Critical Method Stubs
**Duration:** 0.5 days
**Objective:** Create working stubs for all critical methods
#### ✅ **TASK CHECKLIST:**
**1. Data Loading Method Stubs**
```typescript
// Add to store actions
async loadAccountData(): Promise {
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.allMyDids = await retrieveAccountDids();
logger.info('Account data loaded successfully');
} catch (error) {
logger.error("Error loading account data:", error);
this.validationErrors.push("Failed to load account data");
throw error;
}
},
async loadContacts(): Promise {
try {
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
this.allContacts = databaseUtil.mapQueryResultToValues(result) as Contact[];
}
logger.info(`Loaded ${this.allContacts.length} contacts`);
} catch (error) {
logger.error("Error loading contacts:", error);
this.validationErrors.push("Failed to load contacts");
throw error;
}
},
async loadProjects(): Promise {
try {
if (!this.apiServer || !this.activeDid) {
throw new Error('API server or active DID not available');
}
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: ${response.status}`);
}
const results = await response.json();
if (results.data) {
this.projects = results.data;
logger.info(`Loaded ${this.projects.length} projects`);
}
} catch (error) {
logger.error("Error loading projects:", error);
this.validationErrors.push("Failed to load projects");
throw error;
}
}
```
**2. Context Management Method Stubs**
```typescript
// Add to store actions
setCallbackOnSuccess(callback?: (amount: number) => void): void {
this.callbackOnSuccess = callback;
logger.debug('Callback on success set:', !!callback);
},
setCustomTitle(title?: string): void {
this.customTitle = title;
logger.debug('Custom title set:', title);
},
setOfferId(offerId: string): void {
this.offerId = offerId || '';
logger.debug('Offer ID set:', offerId);
},
setPrompt(prompt: string): void {
this.prompt = prompt || '';
logger.debug('Prompt set:', prompt);
}
```
**3. Validation Method Stubs**
```typescript
// Add to store actions
validateCurrentStep(): boolean {
this.validationErrors = [];
switch (this.currentStep) {
case 'entity-selection':
if (!this.giver) this.validationErrors.push('Please select a giver');
if (!this.receiver) this.validationErrors.push('Please select a receiver');
if (this.hasPersonConflict) {
this.validationErrors.push('Cannot select the same person as both giver and recipient');
}
break;
case 'gift-details':
if (!this.description && !parseFloat(this.amountInput)) {
this.validationErrors.push('Please provide description or amount');
}
if (parseFloat(this.amountInput) < 0) {
this.validationErrors.push('Amount cannot be negative');
}
if (!this.activeDid) {
this.validationErrors.push('Active DID required for gift submission');
}
break;
}
const isValid = this.validationErrors.length === 0;
logger.debug('Validation result:', { isValid, errors: this.validationErrors });
return isValid;
}
```
### Pre-Phase 1.4: Integration Testing
**Duration:** 1 day
**Objective:** Test all critical missing functionality before full implementation
#### ✅ **TASK CHECKLIST:**
**1. Create Test Harness**
```typescript
// test-store-functionality.ts (temporary test file)
import { useGiftDialogStore } from '@/stores/giftDialog';
async function testCriticalFunctionality() {
const store = useGiftDialogStore();
console.log('Testing store initialization...');
console.log('Initial state:', store.$state);
console.log('Testing data loading...');
try {
await store.loadAccountData();
console.log('✅ Account data loaded');
await store.loadContacts();
console.log('✅ Contacts loaded');
if (store.allContacts.length > 0) {
console.log('✅ Contacts available:', store.allContacts.length);
}
await store.loadProjects();
console.log('✅ Projects loaded');
} catch (error) {
console.error('❌ Data loading failed:', error);
}
console.log('Testing context management...');
store.setCustomTitle('Test Title');
store.setPrompt('Test Prompt');
store.setOfferId('test-offer-123');
store.setCallbackOnSuccess((amount) => console.log('Success:', amount));
console.log('✅ Context management working');
console.log('Testing validation...');
const isValid = store.validateCurrentStep();
console.log('Validation result:', isValid);
console.log('All critical functionality tests completed!');
}
// Run tests
testCriticalFunctionality().catch(console.error);
```
**2. Manual Integration Tests**
- [ ] Test store can be imported in Vue component
- [ ] Verify all data loading methods work with real data
- [ ] Test context management preserves values
- [ ] Validate error handling works correctly
- [ ] Confirm logging output is helpful for debugging
**3. Performance Validation**
- [ ] Measure store initialization time
- [ ] Test data loading performance with large contact lists
- [ ] Verify no memory leaks in reactive state
- [ ] Check bundle size impact
### Pre-Phase 1.5: Documentation and Checklist Updates
**Duration:** 0.5 days
**Objective:** Update documentation with completed critical fields
#### ✅ **TASK CHECKLIST:**
**1. Update Inventory Status**
- [ ] Mark all critical fields as "✅ Implemented" in annex
- [ ] Update risk assessment to show reduced risk levels
- [ ] Verify readiness status can be changed to "READY"
**2. Create Implementation Checklist**
- [ ] Document exact steps taken for each critical field
- [ ] Create troubleshooting guide for common issues
- [ ] Add performance benchmarks and expectations
**3. Phase 1 Readiness Validation**
- [ ] All external dependencies imported and tested
- [ ] All critical state fields implemented
- [ ] All critical methods have working stubs
- [ ] Store can be instantiated without errors
- [ ] Integration tests pass
### Updated Timeline
#### **Original Timeline:** 5 phases, ~2 weeks
#### **Updated Timeline:** Pre-Phase + 5 phases, ~3 weeks
| Phase | Duration | Status | Dependencies |
|-------|----------|--------|--------------|
| **Pre-Phase 1** | **3 days** | ⏳ **In Progress** | None |
| Phase 1 | 2 days | ⏸️ Waiting | Pre-Phase 1 complete |
| Phase 2 | 3 days | ⏸️ Waiting | Phase 1 complete |
| Phase 3 | 5 days | ⏸️ Waiting | Phase 2 complete |
| Phase 4 | 2 days | ⏸️ Waiting | Phase 3 complete |
| Phase 5 | 2 days | ⏸️ Waiting | Phase 4 complete |
| **Total** | **17 days** | | |
### Risk Mitigation
#### **Pre-Phase 1 Risks:**
1. **Import Path Issues**: Some dependencies might have changed locations
- *Mitigation*: Test each import individually, update paths as needed
2. **Function Signature Changes**: External functions might have different signatures
- *Mitigation*: Validate signatures against actual usage in component
3. **Database Schema Changes**: Contact/project data structures might have evolved
- *Mitigation*: Test with real data, validate type compatibility
4. **Performance Impact**: Adding all fields might impact store performance
- *Mitigation*: Benchmark before/after, optimize if necessary
### Success Criteria for Pre-Phase 1
#### **Must Complete Before Phase 1:**
- [ ] ✅ All 11 external dependencies successfully imported
- [ ] ✅ All 7 critical missing fields added to store interface
- [ ] ✅ All data loading methods working with real data
- [ ] ✅ Context management methods preserve values correctly
- [ ] ✅ Store can be instantiated and used in Vue component
- [ ] ✅ Integration tests pass with 100% success rate
- [ ] ✅ Performance benchmarks within acceptable ranges
- [ ] ✅ Documentation updated to reflect completed work
#### **Readiness Check:**
```typescript
// readiness-check.ts
import { useGiftDialogStore } from '@/stores/giftDialog';
async function verifyReadiness(): Promise {
try {
const store = useGiftDialogStore();
// Test critical functionality
await store.loadAccountData();
await store.loadContacts();
store.setCustomTitle('Readiness Test');
store.setPrompt('Testing prompt');
store.setOfferId('test-123');
const hasAccountData = !!store.activeDid && !!store.apiServer;
const hasContacts = store.allContacts.length >= 0; // Allow empty but defined
const hasContextFields = store.customTitle === 'Readiness Test';
console.log('Readiness Check Results:');
console.log('✅ Account data loaded:', hasAccountData);
console.log('✅ Contacts loaded:', hasContacts);
console.log('✅ Context management:', hasContextFields);
return hasAccountData && hasContacts && hasContextFields;
} catch (error) {
console.error('❌ Readiness check failed:', error);
return false;
}
}
// Usage: await verifyReadiness() should return true before Phase 1
```
**PHASE 1 CAN BEGIN WHEN:** All success criteria are met and readiness check returns `true`.
## GiftedDialog Pinia Store Refactoring Plan
### Overview
This plan outlines a systematic approach to refactor the GiftedDialog component to use Pinia store for state management while maintaining backward compatibility and minimizing disruption. This refactoring also includes improvements to variable naming, type safety, and code clarity.
**CRITICAL UPDATE**: After comprehensive audit of `GiftedDialog.vue`, additional fields and functionality have been identified that must be included in the refactoring plan to prevent breaking changes.
### Phase 1: Foundation Setup
#### 1.1 Create Store Infrastructure
**Files to create:**
```
src/stores/giftDialog.ts
src/types/giftDialog.ts (type definitions)
```
**Tasks:**
- [ ] Define complete `GiftDialogState` interface with all existing fields
- [ ] Create basic Pinia store with initial state
- [ ] Add core getters (`isValid`, `hasPersonConflict`, `formattedAmount`, `shouldShowProjects`)
- [ ] Implement basic actions (`openDialog`, `closeDialog`, `setGiver`, `setReceiver`)
- [ ] Add context management actions (`setCallbackOnSuccess`, `setCustomTitle`, `setOfferId`, `setPrompt`)
- [ ] Add API and account management actions
- [ ] Add simple validation methods
- [ ] **NEW**: Define improved type definitions for entity selection
**Complete Type Definitions:**
```typescript
// src/types/giftDialog.ts
export type EntitySelectionMode = "giver" | "recipient";
export type EntityType = "person" | "project";
export type WorkflowStep = "entity-selection" | "gift-details" | "verification" | "confirmation";
export const ENTITY_SELECTION_MODE = {
GIVER: "giver" as const,
RECIPIENT: "recipient" as const
} as const;
export interface GiftDialogState {
// UI State
isVisible: boolean;
currentStep: WorkflowStep;
entitySelectionMode: EntitySelectionMode; // Renamed from stepType
// Entity State
giverEntityType: EntityType;
recipientEntityType: EntityType;
giver?: GiverReceiverInputInfo;
receiver?: GiverReceiverInputInfo;
// Form State
description: string;
amountInput: string;
unitCode: string;
// Context State (CRITICAL: Missing from original plan)
callbackOnSuccess?: (amount: number) => void;
customTitle?: string;
offerId: string;
prompt: string;
// API and Account State (CRITICAL: Missing from original plan)
apiServer: string;
activeDid: string;
allMyDids: string[];
// Data State
allContacts: Contact[];
projects: PlanData[];
// Validation State
validationErrors: string[];
isSubmitting: boolean;
}
```
**Deliverable:** Working store that can be imported and tested with improved type safety
#### 1.2 Enhanced Store Implementation
**Files to create/modify:**
```
src/stores/giftDialog.ts
```
**Complete Store Actions (CRITICAL ADDITIONS):**
```typescript
export const useGiftDialogStore = defineStore('giftDialog', {
state: (): GiftDialogState => ({
// ... state definition with all fields ...
}),
getters: {
isValid: (state) => {
return state.giver && state.receiver &&
(state.description || parseFloat(state.amountInput) > 0);
},
hasPersonConflict: (state) => {
return state.giver?.did === state.receiver?.did &&
state.giver?.did !== undefined &&
state.giverEntityType === 'person' &&
state.recipientEntityType === 'person';
},
shouldShowProjects: (state) => {
return (state.entitySelectionMode === ENTITY_SELECTION_MODE.GIVER && state.giverEntityType === 'project') ||
(state.entitySelectionMode === ENTITY_SELECTION_MODE.RECIPIENT && state.recipientEntityType === 'project');
},
formattedAmount: (state) => {
const amount = parseFloat(state.amountInput) || 0;
const unit = UNIT_REGISTRY.find(u => u.code === state.unitCode);
return unit ? `${unit.symbol}${amount.toFixed(unit.decimals)}` : amount.toString();
},
giftedDetailsQuery: (state) => {
return {
amountInput: state.amountInput,
description: state.description,
giverDid: state.giverEntityType === "person" ? state.giver?.did : undefined,
giverName: state.giver?.name,
offerId: state.offerId,
fulfillsProjectId: state.giverEntityType === "person" && state.recipientEntityType === "project"
? state.receiver?.handleId : undefined,
providerProjectId: state.giverEntityType === "project" && state.recipientEntityType === "person"
? state.giver?.handleId : undefined,
recipientDid: state.receiver?.did,
recipientName: state.receiver?.name,
unitCode: state.unitCode,
};
}
},
actions: {
// Context Management Actions (CRITICAL ADDITIONS)
setCallbackOnSuccess(callback: (amount: number) => void) {
this.callbackOnSuccess = callback;
},
setCustomTitle(title: string) {
this.customTitle = title;
},
setOfferId(offerId: string) {
this.offerId = offerId;
},
setPrompt(prompt: string) {
this.prompt = prompt;
},
// API and Account Actions (CRITICAL ADDITIONS)
setApiServer(apiServer: string) {
this.apiServer = apiServer;
},
setActiveDid(activeDid: string) {
this.activeDid = activeDid;
},
setAllMyDids(dids: string[]) {
this.allMyDids = dids;
},
setAllContacts(contacts: Contact[]) {
this.allContacts = contacts;
},
setProjects(projects: PlanData[]) {
this.projects = projects;
},
// Enhanced Dialog Management
async openDialog(config: {
giver?: GiverReceiverInputInfo;
receiver?: GiverReceiverInputInfo;
offerId?: string;
customTitle?: string;
prompt?: string;
callbackOnSuccess?: (amount: number) => void;
}) {
this.isVisible = true;
this.giver = config.giver;
this.receiver = config.receiver;
this.offerId = config.offerId || '';
this.customTitle = config.customTitle;
this.prompt = config.prompt || '';
this.callbackOnSuccess = config.callbackOnSuccess;
this.currentStep = config.giver ? 'gift-details' : 'entity-selection';
this.entitySelectionMode = ENTITY_SELECTION_MODE.GIVER;
this.resetValidation();
// Load necessary data
await this.loadAccountData();
await this.loadContacts();
if (this.giverEntityType === 'project' || this.recipientEntityType === 'project') {
await this.loadProjects();
}
},
// Data Loading Actions (CRITICAL ADDITIONS)
async loadAccountData() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.allMyDids = await retrieveAccountDids();
} catch (error) {
logger.error("Error loading account data:", error);
this.validationErrors.push("Failed to load account data");
}
},
async loadContacts() {
try {
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
this.allContacts = databaseUtil.mapQueryResultToValues(result) as Contact[];
}
} catch (error) {
logger.error("Error loading contacts:", error);
this.validationErrors.push("Failed to load contacts");
}
},
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.validationErrors.push("Failed to load projects");
}
},
// Utility Actions
wouldCreateConflict(contactDid: string): boolean {
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
if (this.entitySelectionMode === ENTITY_SELECTION_MODE.GIVER) {
return this.receiver?.did === contactDid;
} else if (this.entitySelectionMode === ENTITY_SELECTION_MODE.RECIPIENT) {
return this.giver?.did === contactDid;
}
return false;
},
increment() {
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
},
decrement() {
this.amountInput = `${Math.max(0, (parseFloat(this.amountInput) || 1) - 1)}`;
},
changeUnitCode() {
const units = Object.keys(UNIT_SHORT);
const index = units.indexOf(this.unitCode);
this.unitCode = units[(index + 1) % units.length];
}
}
});
```
#### 1.3 Store Integration Setup
**Files to modify:**
```
src/main.common.ts
package.json (if Pinia not already installed)
```
**Tasks:**
- [ ] Verify Pinia is installed and configured
- [ ] Add store to main app setup if needed
- [ ] Create simple test to verify store works
- [ ] **NEW**: Set up TypeScript strict mode if not already enabled
- [ ] **CRITICAL**: Import all necessary dependencies in store
**Success Criteria:** Store can be imported and used in any component with full type safety and all existing functionality
### Phase 2: Component Hybrid Implementation
#### 2.1 Create Backward-Compatible GiftedDialog
**Files to modify:**
```
src/components/GiftedDialog.vue
```
**Strategy:** Dual-mode implementation that can work with both old and new patterns while preserving ALL existing functionality
**Tasks:**
- [ ] Add store import and initialization
- [ ] Create computed properties that proxy between component state and store state for ALL fields
- [ ] Add a `useStore` prop to enable/disable store mode
- [ ] Implement store delegation methods alongside existing methods
- [ ] Maintain all existing public methods for backward compatibility
- [ ] **CRITICAL**: Add computed properties for all missing fields identified in audit
- [ ] **NEW**: Rename `stepType` to `entitySelectionMode` in legacy mode
- [ ] **NEW**: Improve method naming consistency
- [ ] **NEW**: Add type safety enhancements
**Complete Implementation with ALL Fields:**
```typescript
export default class GiftedDialog extends Vue {
@Prop({ default: false }) useStore = false;
private giftDialogStore = useGiftDialogStore();
// Legacy state (when useStore = false) - ALL FIELDS
private _visible = false;
private _currentStep = 1;
private _entitySelectionMode: EntitySelectionMode = ENTITY_SELECTION_MODE.GIVER;
private _giverEntityType: EntityType = "person";
private _recipientEntityType: EntityType = "person";
private _giver?: GiverReceiverInputInfo;
private _receiver?: GiverReceiverInputInfo;
private _description = "";
private _amountInput = "0";
private _unitCode = "HUR";
// CRITICAL: Context state fields
private _callbackOnSuccess?: (amount: number) => void;
private _customTitle?: string;
private _offerId = "";
private _prompt = "";
// CRITICAL: API and account state fields
private _apiServer = "";
private _activeDid = "";
private _allMyDids: string[] = [];
private _allContacts: Contact[] = [];
private _projects: PlanData[] = [];
// Computed properties that choose source - ALL FIELDS
get visible() {
return this.useStore ? this.giftDialogStore.isVisible : this._visible;
}
get currentStep() {
return this.useStore ? this.giftDialogStore.currentStep : this._currentStep;
}
get entitySelectionMode() {
return this.useStore ? this.giftDialogStore.entitySelectionMode : this._entitySelectionMode;
}
get giverEntityType() {
return this.useStore ? this.giftDialogStore.giverEntityType : this._giverEntityType;
}
get recipientEntityType() {
return this.useStore ? this.giftDialogStore.recipientEntityType : this._recipientEntityType;
}
get giver() {
return this.useStore ? this.giftDialogStore.giver : this._giver;
}
get receiver() {
return this.useStore ? this.giftDialogStore.receiver : this._receiver;
}
get description() {
return this.useStore ? this.giftDialogStore.description : this._description;
}
get amountInput() {
return this.useStore ? this.giftDialogStore.amountInput : this._amountInput;
}
get unitCode() {
return this.useStore ? this.giftDialogStore.unitCode : this._unitCode;
}
// CRITICAL: Context computed properties
get callbackOnSuccess() {
return this.useStore ? this.giftDialogStore.callbackOnSuccess : this._callbackOnSuccess;
}
get customTitle() {
return this.useStore ? this.giftDialogStore.customTitle : this._customTitle;
}
get offerId() {
return this.useStore ? this.giftDialogStore.offerId : this._offerId;
}
get prompt() {
return this.useStore ? this.giftDialogStore.prompt : this._prompt;
}
// CRITICAL: API and account computed properties
get apiServer() {
return this.useStore ? this.giftDialogStore.apiServer : this._apiServer;
}
get activeDid() {
return this.useStore ? this.giftDialogStore.activeDid : this._activeDid;
}
get allMyDids() {
return this.useStore ? this.giftDialogStore.allMyDids : this._allMyDids;
}
get allContacts() {
return this.useStore ? this.giftDialogStore.allContacts : this._allContacts;
}
get projects() {
return this.useStore ? this.giftDialogStore.projects : this._projects;
}
// Computed properties for derived state
get shouldShowProjects() {
return this.useStore ? this.giftDialogStore.shouldShowProjects :
((this.entitySelectionMode === ENTITY_SELECTION_MODE.GIVER && this.giverEntityType === "project") ||
(this.entitySelectionMode === ENTITY_SELECTION_MODE.RECIPIENT && this.recipientEntityType === "project"));
}
get hasPersonConflict() {
return this.useStore ? this.giftDialogStore.hasPersonConflict :
(this.giver?.did === this.receiver?.did &&
this.giver?.did !== undefined &&
this.giverEntityType === 'person' &&
this.recipientEntityType === 'person');
}
get giftedDetailsQuery() {
return this.useStore ? this.giftDialogStore.giftedDetailsQuery : {
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.receiver?.handleId : undefined,
providerProjectId: this.giverEntityType === "project" && this.recipientEntityType === "person"
? this.giver?.handleId : undefined,
recipientDid: this.receiver?.did,
recipientName: this.receiver?.name,
unitCode: this.unitCode,
};
}
// Enhanced open method to handle ALL parameters
async open(
giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo,
offerId?: string,
customTitle?: string,
prompt?: string,
callbackOnSuccess: (amount: number) => void = () => {},
) {
if (this.useStore) {
await this.giftDialogStore.openDialog({
giver,
receiver,
offerId,
customTitle,
prompt,
callbackOnSuccess
});
} else {
// Legacy implementation with ALL fields
this._customTitle = customTitle;
this._giver = giver;
this._prompt = prompt || "";
this._receiver = receiver;
this._amountInput = "0";
this._callbackOnSuccess = callbackOnSuccess;
this._offerId = offerId || "";
this._currentStep = giver ? 2 : 1;
this._entitySelectionMode = ENTITY_SELECTION_MODE.GIVER;
// Load data in legacy mode
await this.loadLegacyData();
this._visible = true;
}
}
// Legacy data loading method
private async loadLegacyData() {
try {
let 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 Contact[];
}
this._allMyDids = await retrieveAccountDids();
if (this.giverEntityType === "project" || this.recipientEntityType === "project") {
await this.loadLegacyProjects();
}
} catch (err: any) {
logger.error("Error retrieving settings from database:", err);
}
}
private async loadLegacyProjects() {
try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
method: "GET",
headers: await getHeaders(this.activeDid),
});
if (response.status === 200) {
const results = await response.json();
if (results.data) {
this._projects = results.data;
}
}
} catch (error) {
logger.error("Error loading projects:", error);
}
}
// Legacy method support for backward compatibility (KEEP ALL EXISTING METHODS)
goBackToStep1(step: string) {
if (this.useStore) {
this.giftDialogStore.setEntitySelectionMode(step as EntitySelectionMode);
this.giftDialogStore.setCurrentStep('entity-selection');
} else {
this._entitySelectionMode = step as EntitySelectionMode;
this._currentStep = 1;
}
}
selectGiver(contact?: Contact) {
const giverInfo: GiverReceiverInputInfo = contact ? {
did: contact.did,
name: contact.name || contact.did,
} : {
did: "",
name: "Unnamed",
};
if (this.useStore) {
this.giftDialogStore.setGiver(giverInfo);
this.giftDialogStore.setCurrentStep('gift-details');
} else {
this._giver = giverInfo;
this._currentStep = 2;
}
}
selectRecipient(contact?: Contact) {
const recipientInfo: GiverReceiverInputInfo = contact ? {
did: contact.did,
name: contact.name || contact.did,
} : {
did: "",
name: "Unnamed",
};
if (this.useStore) {
this.giftDialogStore.setReceiver(recipientInfo);
this.giftDialogStore.setCurrentStep('gift-details');
} else {
this._receiver = recipientInfo;
this._currentStep = 2;
}
}
selectProject(project: PlanData) {
const giverInfo = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
const receiverInfo = {
did: this.activeDid,
name: "You",
};
if (this.useStore) {
this.giftDialogStore.setGiver(giverInfo);
this.giftDialogStore.setReceiver(receiverInfo);
this.giftDialogStore.setCurrentStep('gift-details');
} else {
this._giver = giverInfo;
this._receiver = receiverInfo;
this._currentStep = 2;
}
}
selectRecipientProject(project: PlanData) {
const recipientInfo = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
if (this.useStore) {
this.giftDialogStore.setReceiver(recipientInfo);
this.giftDialogStore.setCurrentStep('gift-details');
} else {
this._receiver = recipientInfo;
this._currentStep = 2;
}
}
wouldCreateConflict(contactDid: string): boolean {
if (this.useStore) {
return this.giftDialogStore.wouldCreateConflict(contactDid);
} else {
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
if (this.entitySelectionMode === ENTITY_SELECTION_MODE.GIVER) {
return this.receiver?.did === contactDid;
} else if (this.entitySelectionMode === ENTITY_SELECTION_MODE.RECIPIENT) {
return this.giver?.did === contactDid;
}
return false;
}
}
increment() {
if (this.useStore) {
this.giftDialogStore.increment();
} else {
this._amountInput = `${(parseFloat(this._amountInput) || 0) + 1}`;
}
}
decrement() {
if (this.useStore) {
this.giftDialogStore.decrement();
} else {
this._amountInput = `${Math.max(0, (parseFloat(this._amountInput) || 1) - 1)}`;
}
}
changeUnitCode() {
if (this.useStore) {
this.giftDialogStore.changeUnitCode();
} else {
const units = Object.keys(this.libsUtil.UNIT_SHORT);
const index = units.indexOf(this._unitCode);
this._unitCode = units[(index + 1) % units.length];
}
}
// ALL OTHER EXISTING METHODS PRESERVED...
}
```
**Success Criteria:** Component works exactly the same as before when `useStore=false` with ALL functionality preserved
#### 2.2 Create Test View for Store Mode
**Files to create:**
```
src/views/TestGiftDialogStore.vue (temporary)
```
**Tasks:**
- [ ] Create simple test view that uses ``
- [ ] Test all major workflows (open, select entities, submit)
- [ ] Verify store state updates correctly
- [ ] Test validation and error handling
- [ ] **NEW**: Test improved method names and type safety
- [ ] **NEW**: Verify entity selection mode functionality
**Success Criteria:** Store mode works for basic gift creation workflow with improved UX
### Phase 3: Incremental View Migration
#### 3.1 Migrate HomeView
**Files to modify:**
```
src/views/HomeView.vue
```
**Tasks:**
- [ ] Import gift dialog store
- [ ] Update `openDialog` method to use store
- [ ] Change template to use ``
- [ ] Test all gift creation paths from HomeView
- [ ] Verify gift prompts integration still works
- [ ] **NEW**: Update any direct references to old method names
- [ ] **NEW**: Improve error handling with store-based validation
**Enhanced Implementation:**
```typescript
// Updated HomeView.vue with improved integration
import { useGiftDialogStore } from '@/stores/giftDialog';
import { ENTITY_SELECTION_MODE } from '@/types/giftDialog';
export default class HomeView extends Vue {
private giftDialogStore = useGiftDialogStore();
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
if (giver === "Unnamed") {
this.giftDialogStore.openDialog({
receiver: { did: this.activeDid, name: "You" },
prompt: description,
title: "Given by Unnamed"
});
// Immediately select unnamed giver and advance to gift details
this.giftDialogStore.selectAsGiver({ did: "", name: "Unnamed" });
} else {
this.giftDialogStore.openDialog({
giver,
receiver: { did: this.activeDid, name: "You" },
prompt: description,
title: `Given by ${giver?.name || "someone not named"}`
});
}
}
}
```
**Validation:**
- [ ] All existing Playwright tests pass
- [ ] Manual testing of gift creation flows
- [ ] Verify no regressions in user experience
- [ ] **NEW**: Verify improved error messages and validation
#### 3.2 Migrate ContactsView
**Files to modify:**
```
src/views/ContactsView.vue
```
**Tasks:**
- [ ] Update contact-specific gift recording
- [ ] Migrate bidirectional confirmation flows
- [ ] Test contact pre-selection logic
- [ ] Verify direction clarity features work
- [ ] **NEW**: Implement improved entity selection mode handling
- [ ] **NEW**: Update method calls to use new naming convention
#### 3.3 Migrate ProjectViewView
**Files to modify:**
```
src/views/ProjectViewView.vue
```
**Tasks:**
- [ ] Handle dual dialog instances (giveDialogToThis, giveDialogFromThis)
- [ ] Migrate project context locking
- [ ] Update offer integration
- [ ] Test bidirectional gift recording
- [ ] **NEW**: Implement proper entity type management for projects
- [ ] **NEW**: Enhance project-specific validation
#### 3.4 Migrate Remaining Views
**Files to modify:**
```
src/views/ContactGiftingView.vue
src/views/ClaimView.vue
src/views/RecentOffersToUserView.vue
src/views/RecentOffersToUserProjectsView.vue
src/views/NewActivityView.vue
```
**Tasks:**
- [ ] Migrate each view systematically
- [ ] Test specialized features in each view
- [ ] Verify offer fulfillment workflows
- [ ] Ensure activity creation integration works
- [ ] **NEW**: Update all method calls to use improved naming
- [ ] **NEW**: Implement consistent error handling patterns
### Phase 4: Store Enhancement
#### 4.1 Advanced Store Features
**Files to modify:**
```
src/stores/giftDialog.ts
```
**Tasks:**
- [ ] Add comprehensive validation system
- [ ] Implement error handling and recovery
- [ ] Add persistence for draft gifts
- [ ] Create computed properties for complex UI states
- [ ] Add debugging and logging utilities
- [ ] **NEW**: Implement advanced entity selection logic
- [ ] **NEW**: Add conflict resolution strategies
- [ ] **NEW**: Create validation rules engine
#### 4.2 Performance Optimization
**Tasks:**
- [ ] Optimize reactive updates
- [ ] Add selective state subscriptions
- [ ] Implement computed property caching
- [ ] Profile and optimize heavy operations
**Success Criteria:** No performance regressions, ideally improvements
### Phase 5: Cleanup and Finalization
#### 5.1 Remove Legacy Code
**Files to modify:**
```
src/components/GiftedDialog.vue
```
**Tasks:**
- [ ] Remove `useStore` prop and dual-mode logic
- [ ] Delete legacy state variables and methods
- [ ] Simplify component to pure UI logic
- [ ] Update component documentation
- [ ] **NEW**: Remove deprecated method warnings
- [ ] **NEW**: Clean up old variable names completely
- [ ] **NEW**: Finalize improved method signatures
#### 5.2 Template Updates
**Files to modify:**
```
src/components/GiftedDialog.vue (template section)
```
**Tasks:**
- [ ] Update template to use new method names
- [ ] Improve template readability with better variable names
- [ ] Add better accessibility attributes
- [ ] **NEW**: Update all template references to use `entitySelectionMode`
**Template Improvements:**
```vue
```
#### 5.3 Add Comprehensive Testing
**Files to create:**
```
src/stores/__tests__/giftDialog.test.ts
test-playwright/gift-dialog-store.spec.ts
test-playwright/gift-dialog-naming.spec.ts (new)
```
**Tasks:**
- [ ] Unit tests for all store actions and getters
- [ ] Integration tests for complex workflows
- [ ] Error scenario testing
- [ ] Performance regression tests
- [ ] **NEW**: Test improved naming conventions
- [ ] **NEW**: Test entity selection mode functionality
- [ ] **NEW**: Validate type safety improvements
**Enhanced Test Examples:**
```typescript
// Enhanced testing with improved naming
describe('GiftDialogStore Entity Selection', () => {
it('should handle entity selection mode correctly', () => {
const store = useGiftDialogStore();
store.setEntitySelectionMode(ENTITY_SELECTION_MODE.GIVER);
expect(store.entitySelectionMode).toBe(ENTITY_SELECTION_MODE.GIVER);
expect(store.currentSelectionTarget).toBe('giver');
store.setEntitySelectionMode(ENTITY_SELECTION_MODE.RECIPIENT);
expect(store.entitySelectionMode).toBe(ENTITY_SELECTION_MODE.RECIPIENT);
expect(store.currentSelectionTarget).toBe('recipient');
});
it('should detect person conflicts correctly', () => {
const store = useGiftDialogStore();
const person = { did: 'user123', name: 'John' };
store.selectAsGiver(person);
store.selectAsRecipient(person);
expect(store.hasPersonConflict).toBe(true);
expect(store.validationErrors).toContain('Cannot select the same person as both giver and recipient');
});
});
```
#### 5.4 Documentation Updates
**Files to modify:**
```
GiftedDialog-Logic-Flow.md
README.md (if applicable)
```
**Tasks:**
- [ ] Update architecture documentation
- [ ] Document new store-based patterns
- [ ] Create migration guide for future similar refactoring
- [ ] Update developer guidelines
- [ ] **NEW**: Document improved naming conventions
- [ ] **NEW**: Add entity selection mode documentation
- [ ] **NEW**: Create style guide for consistent naming
### Risk Mitigation Strategies
#### Rollback Plan
1. **Feature Flag Approach:** Keep `useStore` prop until migration is complete
2. **Incremental Rollback:** Can revert individual views if issues arise
3. **Branch Strategy:** Maintain working branch alongside feature branch
4. ****NEW**: Backward Compatibility:** Maintain deprecated method warnings during transition
#### Testing Strategy
1. **Continuous Testing:** Run existing Playwright tests after each phase
2. **Manual Testing:** Test critical user journeys after each view migration
3. **Performance Monitoring:** Track bundle size and runtime performance
4. **User Acceptance:** Test with actual users before final deployment
5. ****NEW**: Naming Guide:** Share improved naming conventions with team
6. ****NEW**: Code Review:** Focus on naming consistency in reviews
### Success Metrics
#### Technical Metrics
- [ ] All existing Playwright tests pass
- [ ] No increase in bundle size > 5%
- [ ] No performance regression > 10%
- [ ] Test coverage maintained or improved
- [ ] **NEW**: 100% type safety compliance
- [ ] **NEW**: Zero deprecated method usage in production
#### User Experience Metrics
- [ ] No change in user-facing functionality
- [ ] No increase in error rates
- [ ] Improved developer experience (easier debugging, better error messages)
- [ ] **NEW**: Improved error message clarity
- [ ] **NEW**: Better accessibility scores
#### Code Quality Metrics
- [ ] Reduced component complexity (fewer lines in GiftedDialog.vue)
- [ ] Improved testability (store logic can be tested independently)
- [ ] Better separation of concerns (UI vs business logic)
- [ ] **NEW**: Consistent naming conventions across codebase
- [ ] **NEW**: Improved TypeScript strict mode compliance
- [ ] **NEW**: Reduced cognitive complexity through better naming
### Dependencies and Prerequisites
#### Required Before Starting
- [ ] Pinia already configured in the application
- [ ] All existing tests passing
- [ ] No major pending changes to GiftedDialog
- [ ] Team alignment on approach
- [ ] **NEW**: Team agreement on naming conventions
- [ ] **NEW**: TypeScript strict mode enabled or plan to enable
#### Nice to Have
- [ ] TypeScript strict mode enabled
- [ ] ESLint rules for Pinia best practices
- [ ] Vue DevTools with Pinia extension installed
- [ ] **NEW**: ESLint rules for consistent naming patterns
- [ ] **NEW**: Automated code formatting tools configured
- [ ] **NEW**: Documentation generation tools for type definitions
### Additional Improvements Summary
The enhanced refactoring plan now includes:
1. **Improved Variable Naming**: `stepType` → `entitySelectionMode` with proper typing
2. **Method Name Consistency**: `selectGiver` → `selectAsGiver`, `goBackToStep1` → `goBackToEntitySelection`
3. **Enhanced Type Safety**: Comprehensive TypeScript interfaces and constants
4. **Better Code Organization**: Clear separation of concerns and consistent patterns
5. **Improved Developer Experience**: Better error messages, warnings, and debugging tools
6. **Comprehensive Testing**: Additional test coverage for naming and type safety
7. **Documentation**: Enhanced documentation for new patterns and conventions
This plan provides a systematic, low-risk approach to refactoring while maintaining full backward compatibility and significantly improving code quality and maintainability.
---
## PROPOSED QUICK FIXES AND ENHANCEMENTS
### Overview
During analysis of the GiftedDialog component, several immediate usability improvements were identified. These are documented here as proposed enhancements that can be implemented either as part of the Pinia store refactoring or as separate improvements.
### Critical Usability Issues Identified
#### 1. Component Dual-Purpose Confusion
**Issue**: GiftedDialog serves both entity selection and gift details, which can confuse users about what step they're on.
**Proposed Solution**: Add clear step indicators to show progress through the dialog workflow.
#### 2. Broken Navigation Flow in ContactGiftingView
**Issue**: Contact selection in ContactGiftingView led to dead end - users couldn't complete gift recording and had no clear path back.
**Proposed Solution**: Implement proper success callbacks and navigation logic to return users to appropriate views after gift completion.
#### 3. Redundant Parameters
**Issue**: Legacy `projectId` parameter conflicts with new context-aware parameters, causing confusion.
**Proposed Solution**: Clean up parameter handling and remove redundant legacy parameters.
### Proposed Quick Fixes
#### 1. Navigation Flow Improvements
**Files to modify:**
- `src/views/ContactGiftingView.vue`
- `src/components/GiftedDialog.vue`
**Proposed Changes:**
```typescript
// Add helper methods to ContactGiftingView.vue
buildCurrentGiver(): GiverReceiverInputInfo {
if (this.giverEntityType === "project") {
return {
did: this.giverProjectHandleId,
name: this.giverProjectName,
handleId: this.giverProjectHandleId,
image: this.giverProjectImage
};
}
if (this.giverDid) {
return {
did: this.giverDid,
name: this.giverProjectName || "Someone"
};
}
return { did: this.activeDid, name: "You" };
}
buildCurrentReceiver(): GiverReceiverInputInfo {
if (this.recipientEntityType === "project") {
return {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
handleId: this.recipientProjectHandleId,
image: this.recipientProjectImage
};
}
if (this.recipientDid) {
return {
did: this.recipientDid,
name: "Someone"
};
}
return { did: this.activeDid, name: "You" };
}
buildCustomTitle(contact: Contact | "Unnamed"): string {
const contactName = contact === "Unnamed" ? "Unnamed" : contact.name || "someone";
return this.stepType === "giver"
? `Given by ${contactName}`
: `Given to ${contactName}`;
}
// Improved openDialog method with better navigation flow
openDialogImproved(contact: Contact | "Unnamed") {
const giver = this.stepType === "giver"
? (contact === "Unnamed" ? { did: "", name: "Unnamed" } : contact)
: this.buildCurrentGiver();
const receiver = this.stepType === "recipient"
? (contact === "Unnamed" ? { did: "", name: "Unnamed" } : contact)
: this.buildCurrentReceiver();
// Open dialog with success callback to handle navigation
(this.$refs.customDialog as GiftedDialog).open(
giver,
receiver,
undefined, // offerId
this.buildCustomTitle(contact),
this.prompt,
(amount: number) => {
// Success callback - show success message and navigate back
this.$notify({
group: "alert",
type: "success",
title: "Gift Recorded",
text: `Successfully recorded gift${amount > 0 ? ` of ${amount}` : ''}`
});
// Navigate back to the appropriate view
if (this.isFromProjectView && (this.fromProjectId || this.toProjectId)) {
const projectId = this.fromProjectId || this.toProjectId;
this.$router.push({
name: 'project',
params: { id: projectId }
});
} else {
this.$router.push({ name: 'home' });
}
}
);
// If contact is "Unnamed", immediately advance to step 2
if (contact === "Unnamed") {
if (this.stepType === "giver") {
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
(this.$refs.customDialog as GiftedDialog).selectRecipient();
}
}
}
```
#### 2. Parameter Cleanup
**Proposed Changes:**
- Remove legacy `projectId` parameter from ContactGiftingView
- Eliminate redundant query parameter handling
- Streamline context-aware parameter usage
```typescript
// Remove from ContactGiftingView.vue
// projectId = ""; // Remove this legacy parameter
// Update query parameter handling to remove redundancy
mounted() {
// Remove projectId handling, use only context-specific parameters
this.recipientProjectName =
(this.$route.query["recipientProjectName"] as string) || "";
// ... other parameters without projectId
}
```
#### 3. User Experience Enhancements
**Proposed Template Changes for GiftedDialog.vue:**
```vue
```
### Implementation Priority
#### High Priority (Immediate User Impact)
1. **Navigation Flow Fix**: Implement success callbacks and proper navigation
2. **Step Indicators**: Add visual progress indication
3. **Parameter Cleanup**: Remove legacy `projectId` confusion
#### Medium Priority (User Experience)
1. **Enhanced Labels**: Add color-coded context and emoji visual cues
2. **Error Handling**: Improve error messages and validation feedback
#### Low Priority (Code Quality)
1. **Method Naming**: Standardize method names for consistency
2. **Type Safety**: Enhance TypeScript definitions
3. **Documentation**: Update inline documentation
### Integration with Pinia Refactoring
These quick fixes can be implemented in two ways:
1. **Standalone Implementation**: Apply these fixes to the current component before the Pinia refactoring
2. **Integrated Implementation**: Include these improvements as part of the Pinia store refactoring plan
**Recommendation**: Implement the navigation flow fixes immediately as they address critical usability issues, then include the UX enhancements as part of the Pinia refactoring for better consistency.
### Testing Requirements
#### Manual Testing Checklist
- [ ] ContactGiftingView navigation flow works correctly
- [ ] Users can complete gift recording without getting stuck
- [ ] Success callbacks navigate to appropriate views
- [ ] Step indicators show correct progress
- [ ] Error states are handled gracefully
#### Automated Testing
- [ ] Update Playwright tests to verify navigation flows
- [ ] Add tests for success callback functionality
- [ ] Verify parameter cleanup doesn't break existing functionality
### Risk Assessment
#### Low Risk Changes
- Adding step indicators (purely visual enhancement)
- Improving label text and adding emoji
- Parameter cleanup (removing unused parameters)
#### Medium Risk Changes
- Navigation flow changes (affects user journey)
- Success callback implementation (new functionality)
#### Mitigation Strategies
- Implement changes incrementally
- Test thoroughly in development environment
- Have rollback plan ready
- Monitor user feedback after deployment
**Status**: These improvements are documented as proposed changes and ready for implementation when development resources are available.
---
## TEMPLATE IMPROVEMENTS DURING PINIA REFACTORING
### Overview
The Pinia refactoring provides an excellent opportunity to modernize and significantly improve the GiftedDialog template. The current template has grown organically and contains several areas that can benefit from modern Vue 3 patterns, better accessibility, improved component composition, and cleaner state management integration.
### Current Template Analysis
#### Strengths
- ✅ Responsive grid layouts for different screen sizes
- ✅ Clear visual hierarchy with icons and typography
- ✅ Conflict detection and prevention UI
- ✅ Comprehensive entity selection interface
#### Areas for Improvement
- 🔄 **Complex conditional logic** in template makes it hard to maintain
- 🔄 **Repetitive code** for entity display and selection
- 🔄 **Accessibility gaps** in keyboard navigation and screen readers
- 🔄 **Mixed concerns** - UI logic mixed with business logic
- 🔄 **Inconsistent styling** patterns across similar elements
- 🔄 **No step indicators** for user guidance
- 🔄 **Limited error feedback** for validation states
### Proposed Template Improvements
#### 1. Component Composition and Extraction
**Current Issue**: The template is monolithic with ~400 lines of mixed logic.
**Proposed Solution**: Extract reusable sub-components for better maintainability.
**New Component Structure:**
```vue