Developer Guide
Feature Guides
Goals Feature

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

  1. Enhanced Activity Timeline

    • File attachments to activities
    • Comments on timeline events
    • Rich media support
  2. Advanced Progress Analytics

    • Predictive completion dates
    • Velocity tracking
    • Burndown charts
  3. Collaboration Features

    • Real-time progress updates
    • Team goal synchronization
    • Progress sharing
  4. 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

  1. Update the goalType enum in schema
  2. Implement calculation logic in GoalProgressService
  3. Add UI components for the new type
  4. Update validation rules
  5. Add tests for the new type

Modifying Progress Calculation

  1. Update calculation logic in service
  2. Run migration to recalculate existing goals
  3. Update real-time calculation triggers
  4. 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.