Completed Goals
This document describes the behavior, UI changes, and data handling when a goal is marked as completed/achieved in WealthVN.
Overview
When a user marks a goal as "achieved" (completed), the system transitions the goal to a read-only historical state while preserving all allocation data for reference and analytics.
Key Behaviors
- Allocation Preservation: Completed goals retain their allocations in the database for historical record
- Read-Only Mode: All allocation editing is disabled for completed goals
- Allocation Release: The allocated amounts are "released" back to the unallocated pool
- History Display: Allocation history table shows contribution values at goal completion
Data Model
Goal isAchieved Flag
interface Goal {
id: string;
title: string;
targetAmount: number;
isAchieved: boolean; // Key flag for completed goals
// ... other fields
}
When isAchieved is set to true:
- The goal is considered completed
- UI enters read-only mode
- Allocations are excluded from unallocated balance calculations for other goals
Frontend Implementation
Goal Details Page (goal-details-page.tsx)
The Goal Details Page adapts its layout based on the isAchieved status:
Header Section
For completed goals, a badge is displayed:
{goal.isAchieved && (
<span className="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-sm font-medium text-green-700">
<Icons.Check className="h-4 w-4" />
{t("completedGoal.badge")}
</span>
)}
Description Text
{goal.isAchieved
? t("completedGoal.description")
: t("details.description", { title: goal.title })
}
Hidden Sections
The "Allocation Settings" section (with Edit button) is completely hidden for completed goals:
{!goal.isAchieved && (
<div>
{/* Allocation Settings section */}
</div>
)}
Allocation History
For completed goals, the section title and description change:
<h3>
{goal.isAchieved ? t("completedGoal.allocationHistory") : t("details.allocations.title")}
</h3>
<p>
{goal.isAchieved
? t("completedGoal.historyDescription")
: t("details.allocations.description")
}
</p>
Info Banner
A green info banner explains the allocation release:
{goal.isAchieved && (
<div className="mb-4 flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
<Icons.InfoCircle className="h-4 w-4 shrink-0" />
<span>{t("completedGoal.allocationsReleased")}</span>
</div>
)}
Read-Only Mode
The AllocationHistoryTable receives a readOnly prop:
<AllocationHistoryTable
// ... other props
readOnly={goal.isAchieved}
/>
Goals Page (goals-page.tsx)
Complete Goal Action
Users can mark an active goal as completed:
const handleCompleteGoal = async (goal: Goal) => {
await updateGoalMutation.mutateAsync({
...goal,
isAchieved: true,
});
};
Status Display
The getGoalStatus utility handles completed goals:
export function getGoalStatus(goal: Goal, isOnTrack: boolean) {
if (goal.isAchieved) {
return {
text: "Done",
colorClass: "text-success",
statusText: "Completed",
statusClass: "text-success bg-success/10",
};
}
// ... other status logic
}
Goal Item Component (goal-item.tsx)
Displays visual indicators for completed goals:
// Progress bar color
const progressBarColor = goal.isAchieved ? "bg-primary" : (isOnTrack ? "bg-primary" : "bg-amber-500");
// Status check
if (goal.isAchieved) {
return {
label: "Completed",
colorClass: "text-success",
};
}
Goal Operations (goal-operations.tsx)
The "Complete" action is only shown for non-achieved goals:
{onComplete && !goal.isAchieved && (
<DropdownMenuItem onClick={() => onComplete(goal)}>
<Icons.Check className="mr-2 h-4 w-4" />
{t("item.operations.complete")}
</DropdownMenuItem>
)}
Edit Allocation Modals
Both edit-allocations-modal.tsx and edit-single-allocation-modal.tsx check the goal status:
interface Props {
allGoals?: Goal[]; // All goals for checking isAchieved status
}
// Helper function
const isGoalCompleted = useMemo(() => {
const goalInfo = allGoals?.find(g => g.id === goalId);
return goalInfo?.isAchieved === true;
}, [goalId, allGoals]);
Backend Implementation
Goals Service (goals_service.rs)
Loading All Allocations
The load_goals_allocations method includes completed goals' allocations for display purposes:
fn load_goals_allocations(&self) -> Result<Vec<GoalsAllocation>> {
// Use load_all_allocations to include completed goals' allocations (for display/chart)
self.goal_repo.load_all_allocations()
}
Goals Repository (goals_repository.rs)
Two methods for loading allocations:
/// Load allocations ONLY for active (non-achieved) goals - used for validation
fn load_allocations_for_non_achieved_goals(&self) -> Result<Vec<GoalsAllocation>>;
/// Load ALL allocations including from completed goals (for display purposes)
fn load_all_allocations(&self) -> Result<Vec<GoalsAllocation>>;
Time-Aware Considerations
When calculating unallocated balance for new goals:
- Completed goals' allocations are visible but do NOT reduce the unallocated balance
- Active goals' allocations continue to reduce the unallocated balance
- Overlapping date ranges are checked only for active goals
// In edit-allocations-modal.tsx
const otherActiveAllocations = allAllocations.filter(alloc => {
const goalInfo = allGoals?.find(g => g.id === alloc.goalId);
// Skip allocations from completed goals
if (goalInfo?.isAchieved) return false;
// Skip allocations from the current goal being edited
if (alloc.goalId === goal.id) return false;
return true;
});
Translation Keys
Located in locales/en/goals.json and locales/vi/goals.json:
{
"completedGoal": {
"badge": "Completed",
"description": "This goal has been achieved. All allocations are now released.",
"allocationHistory": "Allocation History",
"historyDescription": "Historical record of account allocations for this completed goal.",
"allocationsReleased": "These allocations have been released. The allocated amounts are now available for other goals."
}
}