Goals Feature Implementation
This document provides technical details about the Goals feature implementation in MTD, including architecture, data models, services, and integration points.
Architecture Overview
The Goals feature follows a layered architecture pattern:
┌─────────────────────────────────────────┐
│ UI Components (React) │
├─────────────────────────────────────────┤
│ Redux State Management │
├─────────────────────────────────────────┤
│ Firebase Services (TypeScript) │
├─────────────────────────────────────────┤
│ Firestore Database │
└─────────────────────────────────────────┘
Data Models
Goal Schema
Location: src/schemas/goal.ts
interface Goal {
gid: string; // Goal ID
name: string;
description?: string;
organizationId: string;
createdBy: string;
createdAt: Timestamp;
updatedAt: Timestamp;
// Timeline (camelCase convention)
startOn?: Timestamp; // Auto-set when first session is logged
dueOn?: Timestamp;
// Goal metrics (nested object with camelCase properties)
metric: {
type: 'TIME_PER_PERIOD' | 'FREQUENCY_PER_PERIOD' | 'NUMERIC' | 'TOTAL_TIME_INVESTED';
target: number;
unit: string;
period?: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'YEARLY';
progressPercent: number; // Calculated progress percentage
currentValue: number; // Current achieved value
};
// Assignment and ownership
assignee?: {
id: string;
name: string;
email: string;
photoUrl?: string;
initials: string;
};
owner: {
id: string;
name: string;
email: string;
photoUrl?: string;
initials: string;
};
// Tracking configuration
linkedEntity?: {
type: 'PILLAR' | 'ACTIVITY';
id: string;
name: string;
color?: { background: string; foreground: string };
icon?: { url?: string; unicode?: string };
};
// Status tracking
status: 'NOT_STARTED' | 'ON_TRACK' | 'AT_RISK' | 'OFF_TRACK' | 'ACHIEVED' | 'MISSED';
// Collaboration
followerIds?: string[];
isPublic?: boolean;
}
Goal Progress Schema
interface GoalProgress {
id: string;
goalId: string;
userId: string;
date: Timestamp;
value: number;
contributingTimeEntryIds: string[];
createdAt: Timestamp;
updatedAt: Timestamp;
}
Goal Status Update Schema
interface GoalStatusUpdate {
id: string;
goalId: string;
userId: string;
content: string; // Rich text content
createdAt: Timestamp;
updatedAt: Timestamp;
}
Service Implementation
GoalsService
Location: src/firebase-services/goals.ts
class GoalsService {
// CRUD Operations
async createGoal(goalData: CreateGoalInput): Promise<Goal> {
// Validate input
// Create goal document
// Initialize progress tracking
// Set up real-time listeners
}
async updateGoal(goalId: string, updates: Partial<Goal>): Promise<void> {
// Validate permissions
// Update goal document
// Recalculate progress if needed
}
async deleteGoal(goalId: string): Promise<void> {
// Soft delete implementation
// Archive related data
}
async getGoal(goalId: string): Promise<Goal> {
// Fetch goal with access control
// Include calculated fields
}
async listGoals(filters: GoalFilters): Promise<Goal[]> {
// Query goals by organization
// Apply filters (status, assignee, date range)
// Sort by priority/date
}
// Progress Calculation
async calculateProgress(goalId: string): Promise<void> {
// Fetch linked time entries
// Calculate based on goal type
// Update progress and status
}
// Real-time Updates
subscribeToGoal(goalId: string, callback: (goal: Goal) => void): Unsubscribe {
// Set up Firestore listener
// Handle real-time updates
}
}
GoalsService Enhanced Features
Location: src/firebase-services/goals.ts
class GoalsService {
// Auto start date functionality
async updateGoalProgressFromTimeEntries(goal: Goal, timeEntries: TimeEntry[]): Promise<void> {
// Auto-set start date when first session is logged
if (!goal.startOn && timeEntries.length > 0) {
const sortedEntries = timeEntries
.filter(entry => entry.from)
.sort((a, b) => {
const dateA = a.from.toDate ? a.from.toDate() : new Date(a.from);
const dateB = b.from.toDate ? b.from.toDate() : new Date(b.from);
return dateA.getTime() - dateB.getTime();
});
if (sortedEntries.length > 0) {
const earliestEntry = sortedEntries[0];
const startDate = Timestamp.fromDate(earliestEntry.from.toDate());
// Update goal with auto-detected start date
await this.updateGoal(goal.gid, { startOn: startDate });
}
}
// Calculate progress with NaN validation
const safeProgress = this.calculateSafeProgress(goal, timeEntries);
await this.updateGoalProgress(goal.gid, safeProgress);
}
// Safe progress calculation with NaN handling
private calculateSafeProgress(goal: Goal, timeEntries: TimeEntry[]): number {
try {
const progress = this.calculateProgressForGoalType(goal, timeEntries);
return isNaN(progress) ? 0 : Math.max(0, Math.min(100, progress));
} catch (error) {
logger.warn('Error calculating progress, defaulting to 0:', error);
return 0;
}
}
// Weekly goal progress calculation for year-long goals
private calculateWeeklyProgress(goal: Goal): number {
if (!goal.startOn || !goal.dueOn) return 0;
const now = new Date();
const startDate = goal.startOn.toDate();
const endDate = goal.dueOn.toDate();
const totalWeeks = Math.ceil((endDate.getTime() - startDate.getTime()) / (7 * 24 * 60 * 60 * 1000));
const elapsedWeeks = Math.ceil((now.getTime() - startDate.getTime()) / (7 * 24 * 60 * 60 * 1000));
const weeklyTarget = (goal.metric.target || 0) / totalWeeks;
const expectedProgress = Math.min(elapsedWeeks * weeklyTarget, goal.metric.target);
return (goal.metric.currentValue / expectedProgress) * 100;
}
}
Routing Architecture
Tab-Based Navigation System
Goals feature implements a comprehensive tab-based routing system:
/goals/[goalGid]/ → Auto-redirects to overview
/goals/[goalGid]/overview/ → Main goal information and progress
/goals/[goalGid]/sessions/ → Time entry management
/goals/[goalGid]/activity/ → Goal history timeline
Auto-Redirect Implementation
Location: src/app/(pages)/(private)/goals/[goalGid]/page.tsx
export default function GoalRedirectPage() {
const params = useParams();
const router = useRouter();
const goalGid = params?.goalGid as string;
useEffect(() => {
if (goalGid) {
router.replace(`/goals/${goalGid}/overview`);
}
}, [goalGid, router]);
return null; // No UI rendered, just redirect
}
Shared Layout Component
Location: src/app/(pages)/(private)/goals/[goalGid]/layout.tsx
const GoalLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const pathname = usePathname();
// Tab determination based on URL
const getCurrentTab = () => {
if (!pathname) return 0;
if (pathname.includes('/overview')) return 0;
if (pathname.includes('/sessions')) return 1;
if (pathname.includes('/activity')) return 2;
return 0;
};
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
const tabPaths = ['/overview', '/sessions', '/activity'];
router.push(`/goals/${goalGid}${tabPaths[newValue]}`);
};
return (
<Page title={goal.name}>
{/* Goal Header with created/started dates */}
<GoalHeader goal={goal} />
{/* Tab Navigation */}
<Tabs value={getCurrentTab()} onChange={handleTabChange}>
<Tab icon={<Timeline />} label="Overview" />
<Tab icon={<ViewList />} label="Sessions" />
<Tab icon={<History />} label="Activity" />
</Tabs>
{/* Tab Content */}
{children}
</Page>
);
};
Component Implementation
Goal Overview Page
Location: src/app/(pages)/(private)/goals/[goalGid]/overview/page.tsx
const GoalOverviewPage: React.FC = () => {
// Main goal information, progress tracking, and statistics
// Enhanced date display with tooltips for "started recently"
// Weekly goal progress calculation for year-long goals
};
Sessions Management Page
Location: src/app/(pages)/(private)/goals/[goalGid]/sessions/page.tsx
const GoalSessionsPage: React.FC = () => {
// Time entry management specific to this goal
// Attach/detach sessions functionality
// Session filtering and search
};
Activity Timeline Page
Location: src/app/(pages)/(private)/goals/[goalGid]/activity/page.tsx
const GoalActivityPage: React.FC = () => {
return (
<Timeline>
{activities.map((activity) => (
<TimelineItem key={activity.id}>
<TimelineSeparator>
<TimelineDot color={getActivityColor(activity.type)} />
<TimelineConnector />
</TimelineSeparator>
<TimelineContent>
<ActivityCard activity={activity} />
</TimelineContent>
</TimelineItem>
))}
</Timeline>
);
};
Goal Progress Component
const GoalProgress: React.FC<{ goal: Goal }> = ({ goal }) => {
const progress = useGoalProgress(goal.id);
return (
<Box>
<LinearProgress
variant="determinate"
value={goal.progressPercentage}
color={getProgressColor(goal.status)}
/>
<Typography>
{formatProgress(goal.currentValue, goal.targetValue, goal.metricType)}
</Typography>
</Box>
);
};
Goal List Component
const GoalsList: React.FC = () => {
const { goals, loading } = useGoals({
status: ['ON_TRACK', 'AT_RISK'],
assigneeIds: [currentUser.id]
});
return (
<DataGrid
rows={goals}
columns={goalColumns}
loading={loading}
onRowClick={(params) => router.push(`/goals/${params.id}`)}
/>
);
};
Integration Points
Time Entry Integration
When time entries are created/updated/deleted:
// In time-entries service
async function handleTimeEntryChange(timeEntry: TimeEntry) {
// Notify goal progress service
await goalProgressService.updateProgressFromTimeEntry(timeEntry);
}
Pillar/Activity Integration
Goals can track specific pillars or activities:
// Link validation
function validateLinkedEntities(goal: CreateGoalInput) {
if (goal.trackingType === 'PILLAR') {
validatePillarIds(goal.linkedPillarIds);
} else if (goal.trackingType === 'ACTIVITY') {
validateActivityIds(goal.linkedActivityIds);
}
}
Notification Integration
// Goal status changes trigger notifications
async function notifyGoalStatusChange(goal: Goal, previousStatus: string) {
const notifications = [];
// Notify assignees
for (const assigneeId of goal.assigneeIds) {
notifications.push(createNotification({
userId: assigneeId,
type: 'GOAL_STATUS_CHANGE',
goalId: goal.id
}));
}
// Notify followers
for (const followerId of goal.followerIds) {
notifications.push(createNotification({
userId: followerId,
type: 'GOAL_UPDATE',
goalId: goal.id
}));
}
await notificationService.sendBatch(notifications);
}
State Management
Redux Slice
Location: src/slices/goals.ts
const goalsSlice = createSlice({
name: 'goals',
initialState: {
goals: [],
currentGoal: null,
loading: false,
error: null
},
reducers: {
setGoals: (state, action) => {
state.goals = action.payload;
},
updateGoalProgress: (state, action) => {
const goal = state.goals.find(g => g.id === action.payload.id);
if (goal) {
goal.currentValue = action.payload.currentValue;
goal.progressPercentage = action.payload.progressPercentage;
}
}
}
});
API Routes
Backend API Endpoints
POST /api/goals # Create goal
GET /api/goals # List goals
GET /api/goals/:id # Get goal details
PUT /api/goals/:id # Update goal
DELETE /api/goals/:id # Delete goal
POST /api/goals/:id/status # Add status update
GET /api/goals/:id/progress # Get progress history
POST /api/goals/:id/follow # Follow/unfollow goal
GET /api/goals/dashboard # Dashboard summary
GET /api/goals/export # Export goals data
Security Considerations
Access Control
// Goal access validation
async function canAccessGoal(userId: string, goal: Goal): boolean {
// Owner can always access
if (goal.createdBy === userId) return true;
// Assignees can access
if (goal.assigneeIds.includes(userId)) return true;
// Organization members can access public goals
if (goal.isPublic && await isInSameOrganization(userId, goal)) return true;
return false;
}
Data Validation
// Input validation
const createGoalSchema = z.object({
name: z.string().min(1).max(200),
goalType: z.enum(['TOTAL_TIME', 'TIME_PER_PERIOD', 'FREQUENCY_PER_PERIOD', 'CUSTOM_NUMERIC']),
targetValue: z.number().positive(),
startDate: z.date(),
endDate: z.date(),
// ... other fields
}).refine(data => data.endDate > data.startDate, {
message: "End date must be after start date"
});
Testing Strategy
Unit Tests
// goals.service.test.ts
describe('GoalsService', () => {
it('should create goal with correct progress tracking', async () => {
const goalData = mockGoalData();
const goal = await goalsService.createGoal(goalData);
expect(goal.currentValue).toBe(0);
expect(goal.status).toBe('NOT_STARTED');
});
it('should calculate progress correctly for each goal type', async () => {
// Test each goal type calculation
});
});
Integration Tests
// goal-time-entry.integration.test.ts
describe('Goal-TimeEntry Integration', () => {
it('should update goal progress when time entry is created', async () => {
const goal = await createTestGoal();
const timeEntry = await createTimeEntry({
activityId: goal.linkedActivityIds[0]
});
const updatedGoal = await goalsService.getGoal(goal.id);
expect(updatedGoal.currentValue).toBeGreaterThan(0);
});
});
Performance Optimizations
Progress Calculation
- Use batch processing for multiple goals
- Cache calculated values with TTL
- Implement incremental updates
// Batch progress calculation
async function batchCalculateProgress(goalIds: string[]) {
const goals = await getGoals(goalIds);
const timeEntries = await getRelevantTimeEntries(goals);
const updates = goals.map(goal => ({
id: goal.id,
progress: calculateGoalProgress(goal, timeEntries)
}));
await batchUpdate(updates);
}
Query Optimization
// Indexed queries
const goalsQuery = db.collection('goals')
.where('organizationId', '==', orgId)
.where('status', 'in', ['ON_TRACK', 'AT_RISK'])
.orderBy('endDate')
.limit(20);
Enhanced Features
Auto Start Date Detection
Goals automatically detect and set their start date when the first time entry is logged:
// Implemented in updateGoalProgressFromTimeEntries
if (!goal.startOn && timeEntries.length > 0) {
const sortedEntries = timeEntries
.filter(entry => entry.from)
.sort((a, b) => dateA.getTime() - dateB.getTime());
if (sortedEntries.length > 0) {
const startDate = Timestamp.fromDate(sortedEntries[0].from.toDate());
await this.updateGoal(goal.gid, { startOn: startDate });
}
}
Enhanced Date Display
Goal headers show both creation and start dates with helpful tooltips:
// Created date with tooltip
<Tooltip title={`Created on ${exactDate}`} arrow>
<Typography variant="caption" color="text.secondary">
Created {formatDistanceToNow(createdDate)} ago
</Typography>
</Tooltip>
// Started date with tooltip
<Tooltip title={`Started on ${exactDate}`} arrow>
<Typography variant="caption" color="primary.main">
Started {formatDistanceToNow(startDate)} ago
</Typography>
</Tooltip>
Activity Timeline
Comprehensive goal history tracking using Material-UI Timeline:
const activities = [
{ type: 'CREATED', timestamp: goal.createdAt, user: goal.owner },
{ type: 'STARTED', timestamp: goal.startOn, user: goal.owner },
{ type: 'STATUS_CHANGE', timestamp: statusUpdate.createdAt,
details: { from: 'NOT_STARTED', to: 'ON_TRACK' } },
{ type: 'SESSION_LOGGED', timestamp: timeEntry.from,
details: { duration: timeEntry.duration, activity: timeEntry.activity } }
];
NaN Validation & Safe Calculations
Comprehensive error handling for numeric calculations:
const safeNum = (value: number, fallback = 0) =>
isNaN(value) ? fallback : Math.round(value);
const progressPercent = safeNum(goal.metric?.progressPercent || 0);
const currentValue = safeNum(goal.metric?.currentValue || 0);
Weekly Goal Logic for Year-Long Goals
Intelligent progress calculation that accounts for time elapsed:
const calculateWeeklyProgress = (goal: Goal) => {
const totalWeeks = getTotalWeeks(goal.startOn, goal.dueOn);
const elapsedWeeks = getElapsedWeeks(goal.startOn, new Date());
const weeklyTarget = goal.metric.target / totalWeeks;
const expectedProgress = Math.min(elapsedWeeks * weeklyTarget, goal.metric.target);
return (goal.metric.currentValue / expectedProgress) * 100;
};
Future Enhancements
Planned Features
-
Enhanced Activity Timeline
- File attachments to activities
- Comments on timeline events
- Rich media support
-
Advanced Progress Analytics
- Predictive completion dates
- Velocity tracking
- Burndown charts
-
Collaboration Features
- Real-time progress updates
- Team goal synchronization
- Progress sharing
-
Integration Extensions
- Calendar integration for deadlines
- Slack/Teams notifications
- Email progress reports
API Extensions
// Future endpoints
POST /api/goals/suggest # AI goal suggestions
GET /api/goals/templates # Goal templates
POST /api/goals/:id/duplicate # Clone goal
GET /api/goals/analytics # Advanced analytics
Development Guidelines
Adding New Goal Types
- Update the
goalType
enum in schema - Implement calculation logic in
GoalProgressService
- Add UI components for the new type
- Update validation rules
- Add tests for the new type
Modifying Progress Calculation
- Update calculation logic in service
- Run migration to recalculate existing goals
- Update real-time calculation triggers
- Test with various scenarios
Performance Monitoring
- Monitor Firestore read/write operations
- Track calculation performance
- Implement logging for debugging
- Set up alerts for anomalies
For questions about the Goals feature implementation, contact the development team or refer to the main Architecture Guide.