Files
maternal-app/docs/implementation-docs/multi-child-implementation-plan.md
Andrei 34b8466004
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
fix: Critical bug fixes for AI chat and children authorization
## AI Chat Fixes
- **CRITICAL**: Fixed AI chat responding only with sleep-related info
  - Root cause: Current user message was never added to context before sending to AI
  - Added user message to context in ai.service.ts before API call
  - Fixed conversation ID handling for new conversations (undefined check)
  - Fixed children query to properly use FamilyMember join instead of incorrect familyId lookup
  - Added FamilyMember entity to AI module imports

- **Context improvements**:
  - New conversations now use empty history array (not the current message)
  - Properly query user's children across all their families via family membership

## Children Authorization Fix
- **CRITICAL SECURITY**: Fixed authorization bug where all users could see all children
  - Root cause: Controllers used `user.sub` but JWT strategy returns `user.userId`
  - Changed all children controller methods to use `user.userId` instead of `user.sub`
  - Added comprehensive logging to track userId and returned children
  - Backend now correctly filters children by family membership

## WebSocket Authentication
- **Enhanced error handling** in families gateway
  - Better error messages for connection failures
  - Added debug logging for token validation
  - More descriptive error emissions to client
  - Added userId fallback (checks both payload.userId and payload.sub)

## User Experience
- **Auto-clear cache on logout**:
  - Logout now clears localStorage and sessionStorage
  - Prevents stale cached data from persisting across sessions
  - Users get fresh data on every login without manual cache clearing

## Testing
- Backend correctly returns only user's own children (verified in logs)
- AI chat now responds to all types of questions, not just sleep-related
- WebSocket authentication provides clearer error feedback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 10:55:25 +00:00

62 KiB

Multi-Child Families - Comprehensive Implementation Plan

Document Overview

This document provides a detailed, actionable implementation plan for enhancing ParentFlow's multi-child support across all application layers: frontend, backend, AI/voice processing, and infrastructure. The plan builds upon the basic multi-child implementation currently in place and the specifications in multi-child-implementation.md.

Current Status: Basic multi-child support exists with child selection in insights/analytics pages.

Goal: Comprehensive multi-child support with dynamic UI, voice commands, AI context awareness, and family-wide analytics.


Table of Contents

  1. Frontend Implementation
  2. Backend Implementation
  3. AI & Voice Processing
  4. Database Schema Updates
  5. State Management Architecture
  6. API Enhancements
  7. Real-Time Sync Updates
  8. Testing Strategy
  9. Migration & Rollout Plan
  10. Performance Optimization

Frontend Implementation

1.1 Global Child Selector Component

Location: /maternal-web/components/common/ChildSelector.tsx

Features:

  • Dropdown/select component with child avatars
  • "All Children" option for aggregate views
  • "Multiple Children" option for bulk operations
  • Persistent selection state across navigation
  • Real-time family member updates
  • Keyboard navigation support
  • Screen reader announcements

Implementation Details:

interface ChildSelectorProps {
  mode: 'single' | 'multiple' | 'all';
  selectedChildIds: string[];
  onChange: (childIds: string[]) => void;
  showAllOption?: boolean;
  showProfilePhotos?: boolean;
  compact?: boolean;
}

// Features to implement:
// 1. Fetch children from Redux store (children slice)
// 2. Show profile photos with fallback avatars
// 3. Support keyboard shortcuts (1-9 for quick select)
// 4. Persist selection to localStorage + Redux
// 5. Announce changes to screen readers
// 6. Show child age alongside name
// 7. Color-code children for consistency across charts

Integration Points:

  • App header (below navigation, above page content)
  • Activity logging flows
  • Analytics pages
  • AI assistant context
  • Voice command feedback

Visibility Rules:

  • Hide when family has only 1 child
  • Show as dropdown for 2-3 children
  • Show as modal/sheet for 4+ children (mobile)
  • Always visible in comparison views

1.2 Dashboard Dynamic Views

1.2.1 Tab View (1-3 Children)

Location: /maternal-web/app/page.tsx (Home Dashboard)

Implementation:

// Add MUI Tabs component at top of dashboard
<Tabs
  value={activeTab}
  onChange={handleTabChange}
  variant="scrollable"
  scrollButtons="auto"
>
  {children.map(child => (
    <Tab
      key={child.id}
      label={child.name}
      icon={<Avatar src={child.photoUrl} />}
      iconPosition="start"
    />
  ))}
  <Tab label="All" icon={<GroupIcon />} iconPosition="start" />
</Tabs>

// Dashboard content filters by selected child(ren)
// Quick actions update to show child-specific data
// Recent activities filter by active tab

Features:

  • Horizontal scrollable tabs for mobile
  • Child avatar + name in each tab
  • "All" tab shows aggregated stats
  • Active tab indicator using theme primary color
  • Smooth transitions between tabs
  • Persist active tab to localStorage
  • Swipe gestures for tab navigation (mobile)

1.2.2 Card View (3+ Children)

Location: /maternal-web/components/features/dashboard/MultiChildCardView.tsx

Implementation:

// Vertical scrollable card list
{children.map(child => (
  <Card key={child.id} sx={{ mb: 1 }}>
    <CardHeader
      avatar={<Avatar src={child.photoUrl}>{child.name[0]}</Avatar>}
      title={child.name}
      subheader={`${calculateAge(child.birthDate)} old`}
    />
    <CardContent>
      {/* Compact metrics */}
      <Grid container spacing={1}>
        <Grid item xs={4}>
          <Stat icon={<BedtimeIcon />} value={todaySleepHours} label="Sleep" />
        </Grid>
        <Grid item xs={4}>
          <Stat icon={<RestaurantIcon />} value={todayFeedings} label="Fed" />
        </Grid>
        <Grid item xs={4}>
          <Stat icon={<ChildCareIcon />} value={todayDiapers} label="Diapers" />
        </Grid>
      </Grid>

      {/* Last activity timestamp */}
      <Typography variant="caption" color="text.secondary">
        Last activity: {formatDistanceToNow(lastActivity.timestamp)} ago
      </Typography>
    </CardContent>
    <CardActions>
      <Button onClick={() => viewChildDetails(child.id)}>View Details</Button>
      <Button onClick={() => logActivityFor(child.id)}>Log Activity</Button>
    </CardActions>
  </Card>
))}

Features:

  • Pull-to-refresh for all cards
  • Click card to expand/navigate to child-specific view
  • Show alerts/notifications badge on cards
  • Color-coded left border per child
  • Skeleton loading states
  • Empty state for no activities today

1.3 Activity Logging Updates

Files to Update:

  • /maternal-web/app/track/feeding/page.tsx
  • /maternal-web/app/track/sleep/page.tsx
  • /maternal-web/app/track/diaper/page.tsx
  • /maternal-web/app/track/activity/page.tsx
  • /maternal-web/app/track/growth/page.tsx

Required Changes:

  1. Child Selection Step (Pre-form)
// Add child selector at top of each tracking form
<Box sx={{ mb: 3, p: 2, bgcolor: 'background.paper', borderRadius: 2 }}>
  <Typography variant="subtitle2" gutterBottom>
    Who is this for?
  </Typography>
  <ChildSelector
    mode="multiple" // Allow bulk logging
    selectedChildIds={selectedChildren}
    onChange={setSelectedChildren}
    showAllOption={false}
  />

  {/* Quick select buttons for last active child */}
  <Button
    size="small"
    onClick={() => setSelectedChildren([lastActiveChildId])}
  >
    Same as last time ({lastActiveChildName})
  </Button>
</Box>
  1. Bulk Logging Support
// When multiple children selected, show batch confirmation
if (selectedChildren.length > 1) {
  return (
    <Alert severity="info" sx={{ mb: 2 }}>
      This will be logged for: {selectedChildren.map(id =>
        children.find(c => c.id === id)?.name
      ).join(', ')}
    </Alert>
  );
}

// Submit handler creates multiple activities
const handleSubmit = async (formData) => {
  const promises = selectedChildren.map(childId =>
    trackingApi.createActivity({
      ...formData,
      childId,
      timestamp: new Date().toISOString(),
    })
  );

  await Promise.all(promises);

  // Show success with child names
  toast.success(`Activity logged for ${selectedChildren.length} children`);
};
  1. Default Child Logic
// On mount, set default child selection:
useEffect(() => {
  // 1. Last child used for this activity type
  const lastUsed = localStorage.getItem(`lastChild_${activityType}`);
  if (lastUsed && children.some(c => c.id === lastUsed)) {
    setSelectedChildren([lastUsed]);
    return;
  }

  // 2. Globally selected child from header
  if (globalSelectedChild) {
    setSelectedChildren([globalSelectedChild]);
    return;
  }

  // 3. First child in family
  if (children.length > 0) {
    setSelectedChildren([children[0].id]);
  }
}, []);

// Save last used child on submit
const saveLastUsedChild = (childId: string) => {
  localStorage.setItem(`lastChild_${activityType}`, childId);
};

1.4 Analytics & Insights Updates

1.4.1 Comparison Tab Component

Location: /maternal-web/components/features/analytics/ComparisonView.tsx

Features:

  • Side-by-side comparison for 2 children
  • Overlay comparison for 3+ children
  • Metric selector (sleep, feeding, growth, development)
  • Date range picker
  • Export comparison report
  • Color-coded legends

Implementation:

interface ComparisonViewProps {
  childIds: string[];
  dateRange: { start: Date; end: Date };
  metrics: ComparisonMetric[];
}

type ComparisonMetric =
  | 'sleep-patterns'
  | 'feeding-frequency'
  | 'growth-curves'
  | 'diaper-changes'
  | 'activities'
  | 'milestones';

// Component structure:
// 1. Child selector (checkboxes for each child)
// 2. Metric selector (tabs or dropdown)
// 3. Date range selector
// 4. Comparison chart area
// 5. Export button

// Chart types based on metric:
// - Sleep: Overlay line chart with different colors
// - Feeding: Bar chart comparison
// - Growth: Multi-line chart with percentile curves
// - Diapers: Stacked bar chart
// - Activities: Heatmap comparison
// - Milestones: Timeline comparison

Chart Color Palette (consistent across app):

const childColors = [
  '#FF6B9D', // Pink
  '#4ECDC4', // Teal
  '#FFD93D', // Yellow
  '#95E1D3', // Mint
  '#C7CEEA', // Lavender
  '#FF8C42', // Orange
  '#A8E6CF', // Green
  '#B8B8FF', // Purple
];

// Assign colors to children consistently
const getChildColor = (childId: string, allChildIds: string[]) => {
  const index = allChildIds.indexOf(childId);
  return childColors[index % childColors.length];
};

1.4.2 Update Existing Analytics Pages

Files:

  • /maternal-web/app/analytics/page.tsx
  • /maternal-web/components/features/analytics/InsightsDashboard.tsx
  • /maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx

Changes:

  1. Add "Compare" tab alongside existing tabs
  2. Show comparison tab only when 2+ children exist
  3. Update all charts to support multi-child overlay
  4. Add child name to chart titles and legends
  5. Export reports include child identifier
// Add tab to analytics page
<Tabs value={tabValue} onChange={setTabValue}>
  <Tab label="Insights" />
  <Tab label="Predictions" />
  {children.length >= 2 && <Tab label="Compare" />}
</Tabs>

// Tab panel for comparison
{tabValue === 2 && children.length >= 2 && (
  <ComparisonView
    childIds={selectedChildIds}
    dateRange={dateRange}
    metrics={['sleep-patterns', 'feeding-frequency']}
  />
)}

1.5 Recent Activities List Enhancement

Location: /maternal-web/components/features/dashboard/RecentActivities.tsx

Updates:

// Add child indicator to each activity item
<ListItem>
  <ListItemAvatar>
    <Avatar
      src={child.photoUrl}
      sx={{
        border: `2px solid ${getChildColor(activity.childId, allChildIds)}`
      }}
    >
      {child.name[0]}
    </Avatar>
  </ListItemAvatar>
  <ListItemText
    primary={
      <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
        <Typography variant="body1">
          {activity.type}
        </Typography>
        {/* Show child name badge */}
        <Chip
          label={child.name}
          size="small"
          sx={{
            bgcolor: alpha(getChildColor(activity.childId, allChildIds), 0.2),
            color: getChildColor(activity.childId, allChildIds),
          }}
        />
      </Box>
    }
    secondary={formatDistanceToNow(activity.timestamp)}
  />
</ListItem>

// Add filter buttons at top
<ButtonGroup size="small" sx={{ mb: 2 }}>
  <Button
    variant={filter === 'all' ? 'contained' : 'outlined'}
    onClick={() => setFilter('all')}
  >
    All
  </Button>
  {children.map(child => (
    <Button
      key={child.id}
      variant={filter === child.id ? 'contained' : 'outlined'}
      onClick={() => setFilter(child.id)}
    >
      {child.name}
    </Button>
  ))}
</ButtonGroup>

1.6 AI Assistant Updates

Location: /maternal-web/app/ai/page.tsx and AI components

Changes:

  1. Context Awareness
// Include selected child in AI context
const buildAIContext = (selectedChildId: string | null) => {
  if (!selectedChildId) {
    return {
      mode: 'general',
      children: children.map(c => ({ id: c.id, name: c.name, age: calculateAge(c.birthDate) })),
    };
  }

  const child = children.find(c => c.id === selectedChildId);
  const recentActivities = activities.filter(a =>
    a.childId === selectedChildId &&
    isAfter(parseISO(a.timestamp), subDays(new Date(), 2))
  );

  return {
    mode: 'child-specific',
    child: {
      id: child.id,
      name: child.name,
      age: calculateAge(child.birthDate),
      gender: child.gender,
    },
    recentActivities: recentActivities.map(a => ({
      type: a.type,
      timestamp: a.timestamp,
      details: a.details,
    })),
  };
};
  1. Multi-Child Comparison Questions
// Support questions like:
// "Why is Emma sleeping less than Noah?"
// "Compare feeding patterns for all my children"
// "Is it normal that Sarah eats more than her brother?"

// AI responses should include child names:
// "Based on Emma's recent sleep data (averaging 11 hours),
//  compared to Noah's 13 hours..."
  1. Child Selector in AI Chat
// Add child selector above chat input
<Box sx={{ mb: 2 }}>
  <Typography variant="caption" color="text.secondary">
    Ask about:
  </Typography>
  <ChildSelector
    mode="single"
    selectedChildIds={[selectedChildForAI]}
    onChange={(ids) => setSelectedChildForAI(ids[0])}
    showAllOption={true}
    compact={true}
  />
</Box>

Backend Implementation

2.1 Children Module Enhancements

Location: /maternal-app-backend/src/modules/children/

Current Status: Basic CRUD operations exist with family-based filtering.

Required Updates:

2.1.1 Color Assignment Service

File: /maternal-app-backend/src/modules/children/children.service.ts

// Auto-assign consistent colors to children
private readonly CHILD_COLORS = [
  '#FF6B9D', '#4ECDC4', '#FFD93D', '#95E1D3',
  '#C7CEEA', '#FF8C42', '#A8E6CF', '#B8B8FF',
];

async create(createChildDto: CreateChildDto): Promise<Child> {
  // Get family's existing children count
  const familyChildren = await this.childRepository.find({
    where: { familyId: createChildDto.familyId, deletedAt: IsNull() },
    order: { createdAt: 'ASC' },
  });

  // Assign color based on birth order
  const colorIndex = familyChildren.length % this.CHILD_COLORS.length;
  const assignedColor = this.CHILD_COLORS[colorIndex];

  const child = this.childRepository.create({
    ...createChildDto,
    displayColor: assignedColor,
  });

  return this.childRepository.save(child);
}

2.1.2 Family Statistics Endpoint

File: /maternal-app-backend/src/modules/children/children.controller.ts

@Get('family/:familyId/statistics')
@UseGuards(JwtAuthGuard)
async getFamilyStatistics(
  @Param('familyId') familyId: string,
  @Request() req,
) {
  // Verify user has access to family
  const hasAccess = await this.familiesService.userHasAccess(req.user.id, familyId);
  if (!hasAccess) {
    throw new ForbiddenException('Access denied to this family');
  }

  const stats = await this.childrenService.getFamilyStatistics(familyId);
  return { success: true, data: stats };
}

// Service implementation
async getFamilyStatistics(familyId: string) {
  const children = await this.findAllForFamily(familyId);

  return {
    totalChildren: children.length,
    viewMode: children.length <= 3 ? 'tabs' : 'cards',
    ageRange: {
      youngest: Math.min(...children.map(c => this.calculateAge(c.birthDate))),
      oldest: Math.max(...children.map(c => this.calculateAge(c.birthDate))),
    },
    genderDistribution: {
      male: children.filter(c => c.gender === 'male').length,
      female: children.filter(c => c.gender === 'female').length,
      other: children.filter(c => !['male', 'female'].includes(c.gender)).length,
    },
  };
}

2.2 Activities/Tracking Module Updates

Location: /maternal-app-backend/src/modules/tracking/

2.2.1 Bulk Activity Creation

File: /maternal-app-backend/src/modules/tracking/tracking.controller.ts

@Post('bulk')
@UseGuards(JwtAuthGuard)
async createBulkActivities(
  @Body() createBulkDto: CreateBulkActivitiesDto,
  @Request() req,
) {
  // Validate user has access to all children
  for (const childId of createBulkDto.childIds) {
    const hasAccess = await this.childrenService.userHasAccessToChild(
      req.user.id,
      childId,
    );
    if (!hasAccess) {
      throw new ForbiddenException(`Access denied to child ${childId}`);
    }
  }

  const activities = await this.trackingService.createBulkActivities(
    createBulkDto,
    req.user.id,
  );

  return { success: true, data: { activities, count: activities.length } };
}

DTO:

// /maternal-app-backend/src/modules/tracking/dto/create-bulk-activities.dto.ts
export class CreateBulkActivitiesDto {
  @IsArray()
  @IsString({ each: true })
  childIds: string[];

  @IsString()
  type: string;

  @IsISO8601()
  timestamp: string;

  @IsOptional()
  @IsObject()
  details?: Record<string, any>;

  @IsOptional()
  @IsString()
  notes?: string;
}

Service:

// /maternal-app-backend/src/modules/tracking/tracking.service.ts
async createBulkActivities(
  dto: CreateBulkActivitiesDto,
  userId: string,
): Promise<Activity[]> {
  const activities = dto.childIds.map(childId =>
    this.activityRepository.create({
      childId,
      type: dto.type,
      timestamp: dto.timestamp,
      details: dto.details,
      notes: dto.notes,
      createdBy: userId,
    }),
  );

  const saved = await this.activityRepository.save(activities);

  // Emit WebSocket events for each child
  for (const activity of saved) {
    this.eventEmitter.emit('activity.created', {
      activity,
      childId: activity.childId,
    });
  }

  return saved;
}

2.2.2 Multi-Child Activity Queries

File: /maternal-app-backend/src/modules/tracking/tracking.controller.ts

@Get()
@UseGuards(JwtAuthGuard)
async getActivities(
  @Query('childIds') childIdsParam?: string, // Comma-separated IDs
  @Query('childId') childId?: string, // Single ID (backward compatibility)
  @Query('type') type?: string,
  @Query('startDate') startDate?: string,
  @Query('endDate') endDate?: string,
  @Request() req,
) {
  // Parse child IDs
  let childIds: string[] = [];
  if (childIdsParam) {
    childIds = childIdsParam.split(',');
  } else if (childId) {
    childIds = [childId];
  }

  // Validate access to all children
  for (const id of childIds) {
    const hasAccess = await this.childrenService.userHasAccessToChild(req.user.id, id);
    if (!hasAccess) {
      throw new ForbiddenException(`Access denied to child ${id}`);
    }
  }

  const activities = await this.trackingService.getActivities({
    childIds,
    type,
    startDate: startDate ? new Date(startDate) : undefined,
    endDate: endDate ? new Date(endDate) : undefined,
  });

  return { success: true, data: activities };
}

2.3 Analytics Module Updates

Location: /maternal-app-backend/src/modules/analytics/

2.3.1 Comparison Endpoint

File: /maternal-app-backend/src/modules/analytics/analytics.controller.ts

@Get('compare')
@UseGuards(JwtAuthGuard)
async compareChildren(
  @Query('childIds') childIdsParam: string, // Required, comma-separated
  @Query('metric') metric: ComparisonMetric,
  @Query('startDate') startDate: string,
  @Query('endDate') endDate: string,
  @Request() req,
) {
  const childIds = childIdsParam.split(',');

  // Validate minimum 2 children
  if (childIds.length < 2) {
    throw new BadRequestException('At least 2 children required for comparison');
  }

  // Validate access
  for (const childId of childIds) {
    const hasAccess = await this.childrenService.userHasAccessToChild(req.user.id, childId);
    if (!hasAccess) {
      throw new ForbiddenException(`Access denied to child ${childId}`);
    }
  }

  const comparison = await this.analyticsService.compareChildren({
    childIds,
    metric,
    startDate: new Date(startDate),
    endDate: new Date(endDate),
  });

  return { success: true, data: comparison };
}

Service Implementation:

// /maternal-app-backend/src/modules/analytics/analytics.service.ts
async compareChildren(params: ComparisonParams): Promise<ComparisonResult> {
  const { childIds, metric, startDate, endDate } = params;

  switch (metric) {
    case 'sleep-patterns':
      return this.compareSleepPatterns(childIds, startDate, endDate);

    case 'feeding-frequency':
      return this.compareFeedingFrequency(childIds, startDate, endDate);

    case 'growth-curves':
      return this.compareGrowthCurves(childIds, startDate, endDate);

    case 'activities':
      return this.compareActivities(childIds, startDate, endDate);

    default:
      throw new BadRequestException(`Unknown metric: ${metric}`);
  }
}

private async compareSleepPatterns(
  childIds: string[],
  startDate: Date,
  endDate: Date,
): Promise<SleepComparisonResult> {
  const dataByChild = await Promise.all(
    childIds.map(async (childId) => {
      const sleepActivities = await this.activityRepository.find({
        where: {
          childId,
          type: 'sleep',
          timestamp: Between(startDate, endDate),
        },
        order: { timestamp: 'ASC' },
      });

      return {
        childId,
        data: this.aggregateSleepData(sleepActivities),
      };
    }),
  );

  return {
    metric: 'sleep-patterns',
    dateRange: { start: startDate, end: endDate },
    children: dataByChild,
    summary: this.calculateSleepSummary(dataByChild),
  };
}

2.3.2 Aggregated Statistics

File: /maternal-app-backend/src/modules/analytics/analytics.service.ts

async getAggregatedStatistics(
  familyId: string,
  startDate: Date,
  endDate: Date,
): Promise<AggregatedStats> {
  const children = await this.childrenService.findAllForFamily(familyId);

  const childStats = await Promise.all(
    children.map(child =>
      this.getChildStatistics(child.id, startDate, endDate)
    ),
  );

  return {
    familyId,
    dateRange: { start: startDate, end: endDate },
    totalChildren: children.length,
    perChild: childStats,
    aggregate: {
      totalActivities: childStats.reduce((sum, s) => sum + s.totalActivities, 0),
      totalSleepHours: childStats.reduce((sum, s) => sum + s.totalSleepHours, 0),
      totalFeedings: childStats.reduce((sum, s) => sum + s.totalFeedings, 0),
      averageSleepPerChild: childStats.reduce((sum, s) => sum + s.totalSleepHours, 0) / children.length,
    },
  };
}

AI & Voice Processing

3.1 Voice Command Multi-Child Detection

Location: /maternal-app-backend/src/modules/ai/services/voice-processing.service.ts

Current Status: Basic voice-to-text exists. Needs child name extraction.

3.1.1 Child Name Entity Recognition

interface VoiceProcessingResult {
  transcription: string;
  detectedChildIds: string[];
  confidence: number;
  activityType?: string;
  activityDetails?: Record<string, any>;
  requiresClarity?: boolean;
  clarificationPrompt?: string;
}

@Injectable()
export class VoiceProcessingService {
  private readonly MULTI_CHILD_KEYWORDS = [
    'both', 'all', 'everyone', 'kids', 'children', 'twins', 'both kids',
  ];

  async processVoiceInput(
    audioBuffer: Buffer,
    familyId: string,
  ): Promise<VoiceProcessingResult> {
    // 1. Transcribe audio using Whisper API
    const transcription = await this.transcribeAudio(audioBuffer);

    // 2. Get family's children
    const children = await this.childrenService.findAllForFamily(familyId);

    // 3. Detect child names in transcription
    const detectedChildren = this.detectChildNames(transcription, children);

    // 4. Check for multi-child keywords
    const hasMultiKeyword = this.MULTI_CHILD_KEYWORDS.some(keyword =>
      transcription.toLowerCase().includes(keyword),
    );

    if (hasMultiKeyword) {
      // Apply to all children
      return {
        transcription,
        detectedChildIds: children.map(c => c.id),
        confidence: 0.9,
        activityType: this.extractActivityType(transcription),
      };
    }

    if (detectedChildren.length === 0) {
      // No child detected - need clarification
      return {
        transcription,
        detectedChildIds: [],
        confidence: 0.0,
        requiresClarity: true,
        clarificationPrompt: 'Which child is this for?',
      };
    }

    if (detectedChildren.length === 1) {
      // Single child detected
      return {
        transcription,
        detectedChildIds: [detectedChildren[0].id],
        confidence: 0.95,
        activityType: this.extractActivityType(transcription),
        activityDetails: this.extractActivityDetails(transcription),
      };
    }

    // Multiple children detected
    return {
      transcription,
      detectedChildIds: detectedChildren.map(c => c.id),
      confidence: 0.85,
      activityType: this.extractActivityType(transcription),
    };
  }

  private detectChildNames(
    text: string,
    children: Child[],
  ): Child[] {
    const lowerText = text.toLowerCase();
    const detected: Child[] = [];

    for (const child of children) {
      const nameLower = child.name.toLowerCase();

      // Exact name match
      if (lowerText.includes(nameLower)) {
        detected.push(child);
        continue;
      }

      // Nickname/partial match (first name only if multiple words)
      const firstName = child.name.split(' ')[0].toLowerCase();
      if (lowerText.includes(firstName)) {
        detected.push(child);
      }
    }

    return detected;
  }

  private extractActivityType(text: string): string | undefined {
    const lowerText = text.toLowerCase();

    const activityPatterns = [
      { pattern: /\b(fed|feeding|ate|eat|bottle|breast|milk)\b/, type: 'feeding' },
      { pattern: /\b(sleep|sleeping|nap|napping|asleep)\b/, type: 'sleep' },
      { pattern: /\b(diaper|pee|poop|changed)\b/, type: 'diaper' },
      { pattern: /\b(play|playing|played)\b/, type: 'play' },
      { pattern: /\b(bath|bathing|bathed)\b/, type: 'bath' },
    ];

    for (const { pattern, type } of activityPatterns) {
      if (pattern.test(lowerText)) {
        return type;
      }
    }

    return undefined;
  }
}

3.1.2 Voice Clarification Flow

Frontend Component: /maternal-web/components/features/voice/VoiceClarification.tsx

interface VoiceClarificationProps {
  transcription: string;
  prompt: string;
  children: Child[];
  onConfirm: (childIds: string[]) => void;
  onCancel: () => void;
}

// Modal/Dialog showing:
// 1. Transcription: "Emma just had a bottle"
// 2. Detected uncertainty: "Which Emma?"
// 3. Child selector buttons with avatars
// 4. Voice response option: "Say the child's full name"

3.2 AI Assistant Context Building

Location: /maternal-app-backend/src/modules/ai/services/context-builder.service.ts

3.2.1 Multi-Child Context Strategy

interface AIContext {
  mode: 'single-child' | 'multi-child' | 'comparison' | 'general';
  children: ChildContext[];
  selectedChildId?: string;
  tokenBudget: number;
  priorityWeights: Record<string, number>;
}

interface ChildContext {
  childId: string;
  name: string;
  age: string;
  gender: string;
  recentActivities: Activity[];
  patterns: PatternInsights;
  medicalInfo?: Record<string, any>;
}

@Injectable()
export class ContextBuilderService {
  private readonly TOKEN_BUDGET = 4000;

  async buildContext(params: {
    childIds?: string[];
    mode: AIContext['mode'];
    query: string;
  }): Promise<AIContext> {
    const { childIds, mode, query } = params;

    if (mode === 'general' || !childIds || childIds.length === 0) {
      return this.buildGeneralContext(query);
    }

    if (mode === 'single-child' && childIds.length === 1) {
      return this.buildSingleChildContext(childIds[0], query);
    }

    if (mode === 'multi-child' || mode === 'comparison') {
      return this.buildMultiChildContext(childIds, query);
    }
  }

  private async buildSingleChildContext(
    childId: string,
    query: string,
  ): Promise<AIContext> {
    const child = await this.childrenService.findOne(childId);
    const recentActivities = await this.getRecentActivities(childId, 48); // 48 hours
    const patterns = await this.analyticsService.getPatterns(childId, 7); // 7 days

    return {
      mode: 'single-child',
      selectedChildId: childId,
      children: [{
        childId: child.id,
        name: child.name,
        age: this.calculateAge(child.birthDate),
        gender: child.gender,
        recentActivities: this.prioritizeActivities(recentActivities, query),
        patterns,
        medicalInfo: child.medicalInfo,
      }],
      tokenBudget: this.TOKEN_BUDGET,
      priorityWeights: {
        query: 1.0,
        recentActivities: 0.8,
        childProfile: 0.7,
        patterns: 0.6,
      },
    };
  }

  private async buildMultiChildContext(
    childIds: string[],
    query: string,
  ): Promise<AIContext> {
    // Distribute token budget across children
    const tokensPerChild = Math.floor(this.TOKEN_BUDGET * 0.7 / childIds.length);

    const childrenContexts = await Promise.all(
      childIds.map(async (childId) => {
        const child = await this.childrenService.findOne(childId);
        const recentActivities = await this.getRecentActivities(childId, 24); // 24h for multi

        return {
          childId: child.id,
          name: child.name,
          age: this.calculateAge(child.birthDate),
          gender: child.gender,
          recentActivities: recentActivities.slice(0, 5), // Limit activities
          patterns: await this.analyticsService.getPatterns(childId, 7),
        };
      }),
    );

    return {
      mode: 'multi-child',
      children: childrenContexts,
      tokenBudget: this.TOKEN_BUDGET,
      priorityWeights: {
        query: 1.0,
        childComparison: 0.9,
        recentActivities: 0.6,
        patterns: 0.5,
      },
    };
  }

  private prioritizeActivities(
    activities: Activity[],
    query: string,
  ): Activity[] {
    // Filter activities based on query relevance
    const queryLower = query.toLowerCase();

    const scored = activities.map(activity => {
      let score = 1.0;

      // Boost if activity type mentioned in query
      if (queryLower.includes(activity.type)) {
        score += 2.0;
      }

      // Boost recent activities
      const hoursAgo = differenceInHours(new Date(), parseISO(activity.timestamp));
      score += Math.max(0, (48 - hoursAgo) / 48);

      return { activity, score };
    });

    return scored
      .sort((a, b) => b.score - a.score)
      .slice(0, 10)
      .map(s => s.activity);
  }
}

3.2.2 AI Prompt Templates for Multi-Child

File: /maternal-app-backend/src/modules/ai/prompts/multi-child-prompts.ts

export const MULTI_CHILD_SYSTEM_PROMPT = `
You are an AI parenting assistant helping a family with multiple children.

When answering questions:
1. ALWAYS mention the child's name when referring to specific data
2. Use comparison language when discussing multiple children
3. Acknowledge individual differences between children
4. Avoid making one child seem "better" or "worse"
5. Provide age-appropriate context for each child

Family context:
{{#each children}}
- {{name}} ({{age}} old, {{gender}})
{{/each}}

User's question: {{query}}
`;

export const COMPARISON_PROMPT_TEMPLATE = `
Compare the following metric for these children:
{{#each children}}
Child: {{name}} ({{age}})
{{metric}} data: {{data}}
{{/each}}

Provide a balanced comparison that:
1. Highlights patterns for each child
2. Explains any differences based on age, development, or recent events
3. Offers actionable insights
4. Reassures the parent that variations are normal
`;

export const MULTI_CHILD_ACTIVITY_TEMPLATE = `
The user reported: "{{transcription}}"

Detected children: {{#each detectedChildren}}{{name}}{{/each}}
Activity type: {{activityType}}

Generate a natural confirmation message that:
1. Repeats back what was logged
2. Mentions all children involved
3. Provides brief context (e.g., "that's their 3rd feeding today")
`;

Database Schema Updates

4.1 Children Table Enhancement

Migration: /maternal-app-backend/src/database/migrations/V008_add_child_display_preferences.sql

-- Add display color column
ALTER TABLE children
ADD COLUMN display_color VARCHAR(7) DEFAULT '#FF6B9D',
ADD COLUMN sort_order INTEGER DEFAULT 0,
ADD COLUMN nickname VARCHAR(50);

-- Update existing children with colors based on creation order
WITH numbered_children AS (
  SELECT
    id,
    ROW_NUMBER() OVER (PARTITION BY family_id ORDER BY created_at) - 1 AS row_num
  FROM children
  WHERE deleted_at IS NULL
)
UPDATE children c
SET display_color = CASE (nc.row_num % 8)
  WHEN 0 THEN '#FF6B9D'
  WHEN 1 THEN '#4ECDC4'
  WHEN 2 THEN '#FFD93D'
  WHEN 3 THEN '#95E1D3'
  WHEN 4 THEN '#C7CEEA'
  WHEN 5 THEN '#FF8C42'
  WHEN 6 THEN '#A8E6CF'
  WHEN 7 THEN '#B8B8FF'
END,
sort_order = nc.row_num
FROM numbered_children nc
WHERE c.id = nc.id;

-- Create index for family queries
CREATE INDEX idx_children_family_sort ON children(family_id, sort_order, deleted_at);

4.2 User Preferences for Multi-Child

Migration: /maternal-app-backend/src/database/migrations/V009_create_user_preferences.sql

CREATE TABLE user_preferences (
  id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  preference_key VARCHAR(100) NOT NULL,
  preference_value JSONB NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  UNIQUE(user_id, preference_key)
);

CREATE INDEX idx_user_preferences_user ON user_preferences(user_id);

-- Seed default preferences for multi-child families
INSERT INTO user_preferences (user_id, preference_key, preference_value)
SELECT DISTINCT u.id, 'multi_child_view_mode', '"auto"'
FROM users u
INNER JOIN family_members fm ON u.id = fm.user_id
INNER JOIN children c ON fm.family_id = c.family_id
WHERE c.deleted_at IS NULL
GROUP BY u.id
HAVING COUNT(DISTINCT c.id) >= 2
ON CONFLICT (user_id, preference_key) DO NOTHING;

-- Preferences to store:
-- - multi_child_view_mode: "tabs" | "cards" | "auto"
-- - default_child_id: child_id for quick actions
-- - last_selected_child_per_screen: { "/track/feeding": "child_id", ... }
-- - comparison_default_children: ["child_id_1", "child_id_2"]
-- - comparison_default_metric: "sleep-patterns"

4.3 Activity Association Validation

Migration: /maternal-app-backend/src/database/migrations/V010_add_activity_constraints.sql

-- Ensure all activities have valid child_id
ALTER TABLE activities
ADD CONSTRAINT fk_activities_child
FOREIGN KEY (child_id)
REFERENCES children(id)
ON DELETE CASCADE;

-- Add index for multi-child queries
CREATE INDEX idx_activities_multiple_children
ON activities(child_id, type, timestamp DESC);

-- Add check constraint for future bulk operation tracking
ALTER TABLE activities
ADD COLUMN bulk_operation_id VARCHAR(36),
ADD COLUMN siblings_count INTEGER DEFAULT 1;

CREATE INDEX idx_activities_bulk_operation
ON activities(bulk_operation_id)
WHERE bulk_operation_id IS NOT NULL;

COMMENT ON COLUMN activities.bulk_operation_id IS
'Groups activities logged simultaneously for multiple children';
COMMENT ON COLUMN activities.siblings_count IS
'Number of children this activity was logged for (for UI display)';

State Management Architecture

5.1 Redux Store Updates

5.1.1 Children Slice Enhancement

File: /maternal-web/lib/store/slices/childrenSlice.ts

interface ChildrenState {
  byId: Record<string, Child>;
  allIds: string[];
  selectedChildId: string | null;
  selectedChildIds: string[]; // Multi-select support
  defaultChildId: string | null; // For quick actions
  viewMode: 'tabs' | 'cards' | 'auto';
  loading: boolean;
  error: string | null;
  lastUpdated: string | null;
}

const childrenSlice = createSlice({
  name: 'children',
  initialState: {
    byId: {},
    allIds: [],
    selectedChildId: null,
    selectedChildIds: [],
    defaultChildId: null,
    viewMode: 'auto',
    loading: false,
    error: null,
    lastUpdated: null,
  } as ChildrenState,
  reducers: {
    setSelectedChild: (state, action: PayloadAction<string>) => {
      state.selectedChildId = action.payload;
      state.selectedChildIds = [action.payload];

      // Save to localStorage
      if (typeof window !== 'undefined') {
        localStorage.setItem('selectedChildId', action.payload);
      }
    },

    setSelectedChildren: (state, action: PayloadAction<string[]>) => {
      state.selectedChildIds = action.payload;
      state.selectedChildId = action.payload[0] || null;
    },

    toggleChildSelection: (state, action: PayloadAction<string>) => {
      const childId = action.payload;
      const index = state.selectedChildIds.indexOf(childId);

      if (index >= 0) {
        state.selectedChildIds.splice(index, 1);
      } else {
        state.selectedChildIds.push(childId);
      }

      state.selectedChildId = state.selectedChildIds[0] || null;
    },

    setDefaultChild: (state, action: PayloadAction<string>) => {
      state.defaultChildId = action.payload;
      localStorage.setItem('defaultChildId', action.payload);
    },

    setViewMode: (state, action: PayloadAction<'tabs' | 'cards' | 'auto'>) => {
      state.viewMode = action.payload;
      localStorage.setItem('childViewMode', action.payload);
    },

    calculateViewMode: (state) => {
      if (state.viewMode === 'auto') {
        return state.allIds.length <= 3 ? 'tabs' : 'cards';
      }
      return state.viewMode;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchChildren.fulfilled, (state, action) => {
        state.byId = action.payload.reduce((acc, child) => {
          acc[child.id] = child;
          return acc;
        }, {} as Record<string, Child>);
        state.allIds = action.payload.map(c => c.id);

        // Auto-select first child if none selected
        if (!state.selectedChildId && state.allIds.length > 0) {
          state.selectedChildId = state.allIds[0];
          state.selectedChildIds = [state.allIds[0]];
        }

        state.loading = false;
        state.lastUpdated = new Date().toISOString();
      });
  },
});

// Selectors
export const selectAllChildren = (state: RootState) =>
  state.children.allIds.map(id => state.children.byId[id]);

export const selectSelectedChild = (state: RootState) =>
  state.children.selectedChildId
    ? state.children.byId[state.children.selectedChildId]
    : null;

export const selectSelectedChildren = (state: RootState) =>
  state.children.selectedChildIds.map(id => state.children.byId[id]);

export const selectChildrenCount = (state: RootState) =>
  state.children.allIds.length;

export const selectViewMode = (state: RootState) => {
  if (state.children.viewMode === 'auto') {
    return state.children.allIds.length <= 3 ? 'tabs' : 'cards';
  }
  return state.children.viewMode;
};

export const selectChildColor = (childId: string) => (state: RootState) =>
  state.children.byId[childId]?.displayColor || '#FF6B9D';

5.1.2 UI Slice for Per-Screen Child Selection

File: /maternal-web/lib/store/slices/uiSlice.ts

interface UIState {
  // ... existing UI state
  lastSelectedChildPerScreen: Record<string, string>; // { "/track/feeding": "child_id" }
  comparisonPreferences: {
    selectedChildIds: string[];
    metric: ComparisonMetric;
    dateRange: { start: string; end: string };
  };
}

const uiSlice = createSlice({
  name: 'ui',
  initialState: {
    // ... existing state
    lastSelectedChildPerScreen: {},
    comparisonPreferences: {
      selectedChildIds: [],
      metric: 'sleep-patterns',
      dateRange: {
        start: subDays(new Date(), 7).toISOString(),
        end: new Date().toISOString(),
      },
    },
  } as UIState,
  reducers: {
    setLastSelectedChildForScreen: (
      state,
      action: PayloadAction<{ screen: string; childId: string }>
    ) => {
      state.lastSelectedChildPerScreen[action.payload.screen] = action.payload.childId;
      localStorage.setItem(
        'lastSelectedChildPerScreen',
        JSON.stringify(state.lastSelectedChildPerScreen)
      );
    },

    setComparisonPreferences: (
      state,
      action: PayloadAction<Partial<UIState['comparisonPreferences']>>
    ) => {
      state.comparisonPreferences = {
        ...state.comparisonPreferences,
        ...action.payload,
      };
      localStorage.setItem(
        'comparisonPreferences',
        JSON.stringify(state.comparisonPreferences)
      );
    },
  },
});

export const selectLastChildForScreen = (screen: string) => (state: RootState) =>
  state.ui.lastSelectedChildPerScreen[screen];

API Enhancements

6.1 API Client Updates

File: /maternal-web/lib/api/children.ts

// Add method for family statistics
export const childrenApi = {
  // ... existing methods

  async getFamilyStatistics(familyId: string) {
    const response = await apiClient.get(
      `/children/family/${familyId}/statistics`
    );
    return response.data.data;
  },

  async getChildrenWithColors(familyId: string): Promise<Child[]> {
    const children = await this.getChildren(familyId);
    // Colors now come from backend
    return children;
  },
};

File: /maternal-web/lib/api/tracking.ts

export const trackingApi = {
  // ... existing methods

  async createBulkActivities(params: {
    childIds: string[];
    type: string;
    timestamp: string;
    details?: Record<string, any>;
    notes?: string;
  }) {
    const response = await apiClient.post('/tracking/bulk', params);
    return response.data.data;
  },

  async getActivitiesForMultipleChildren(params: {
    childIds: string[];
    type?: string;
    startDate?: string;
    endDate?: string;
  }) {
    const response = await apiClient.get('/tracking', {
      params: {
        childIds: params.childIds.join(','),
        type: params.type,
        startDate: params.startDate,
        endDate: params.endDate,
      },
    });
    return response.data.data;
  },
};

File: /maternal-web/lib/api/analytics.ts

export const analyticsApi = {
  // ... existing methods

  async compareChildren(params: {
    childIds: string[];
    metric: ComparisonMetric;
    startDate: string;
    endDate: string;
  }) {
    const response = await apiClient.get('/analytics/compare', {
      params: {
        childIds: params.childIds.join(','),
        metric: params.metric,
        startDate: params.startDate,
        endDate: params.endDate,
      },
    });
    return response.data.data;
  },

  async getAggregatedStatistics(params: {
    familyId: string;
    startDate: string;
    endDate: string;
  }) {
    const response = await apiClient.get('/analytics/aggregated', {
      params,
    });
    return response.data.data;
  },
};

Real-Time Sync Updates

7.1 WebSocket Event Enhancements

File: /maternal-app-backend/src/modules/websocket/websocket.gateway.ts

@WebSocketGateway({
  cors: { origin: '*' },
  namespace: 'family',
})
export class WebSocketGateway implements OnGatewayConnection {
  @WebSocketServer()
  server: Server;

  // Join child-specific rooms
  @SubscribeMessage('join-child-room')
  handleJoinChildRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { childId: string },
  ) {
    const room = `child:${data.childId}`;
    client.join(room);
    this.logger.log(`Client ${client.id} joined room ${room}`);
  }

  @SubscribeMessage('leave-child-room')
  handleLeaveChildRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { childId: string },
  ) {
    const room = `child:${data.childId}`;
    client.leave(room);
  }

  // Emit activity updates to child-specific rooms
  emitActivityCreated(activity: Activity) {
    this.server
      .to(`child:${activity.childId}`)
      .emit('activity:created', {
        activity,
        childId: activity.childId,
        timestamp: new Date().toISOString(),
      });
  }

  emitBulkActivitiesCreated(activities: Activity[]) {
    // Group by child and emit separately
    const byChild = activities.reduce((acc, activity) => {
      if (!acc[activity.childId]) {
        acc[activity.childId] = [];
      }
      acc[activity.childId].push(activity);
      return acc;
    }, {} as Record<string, Activity[]>);

    Object.entries(byChild).forEach(([childId, childActivities]) => {
      this.server
        .to(`child:${childId}`)
        .emit('activities:bulk-created', {
          activities: childActivities,
          childId,
          count: childActivities.length,
        });
    });
  }
}

Frontend WebSocket Client: /maternal-web/lib/websocket/client.ts

export class WebSocketClient {
  private socket: Socket | null = null;
  private subscribedChildIds: Set<string> = new Set();

  connect(userId: string, familyId: string) {
    this.socket = io(`${WS_URL}/family`, {
      auth: { userId, familyId },
    });

    this.setupEventListeners();
  }

  subscribeToChild(childId: string) {
    if (!this.subscribedChildIds.has(childId)) {
      this.socket?.emit('join-child-room', { childId });
      this.subscribedChildIds.add(childId);
    }
  }

  unsubscribeFromChild(childId: string) {
    if (this.subscribedChildIds.has(childId)) {
      this.socket?.emit('leave-child-room', { childId });
      this.subscribedChildIds.delete(childId);
    }
  }

  subscribeToSelectedChildren(childIds: string[]) {
    // Unsubscribe from old children
    this.subscribedChildIds.forEach(id => {
      if (!childIds.includes(id)) {
        this.unsubscribeFromChild(id);
      }
    });

    // Subscribe to new children
    childIds.forEach(id => this.subscribeToChild(id));
  }

  private setupEventListeners() {
    this.socket?.on('activity:created', (data) => {
      store.dispatch(activityCreated(data.activity));
    });

    this.socket?.on('activities:bulk-created', (data) => {
      store.dispatch(bulkActivitiesCreated(data.activities));
    });
  }
}

// Hook for components
export const useWebSocketChildSync = (childIds: string[]) => {
  useEffect(() => {
    wsClient.subscribeToSelectedChildren(childIds);

    return () => {
      childIds.forEach(id => wsClient.unsubscribeFromChild(id));
    };
  }, [childIds]);
};

Testing Strategy

8.1 Unit Tests

8.1.1 Child Selection Logic

File: /maternal-web/__tests__/store/childrenSlice.test.ts

describe('childrenSlice', () => {
  it('should calculate view mode based on child count', () => {
    const state = {
      ...initialState,
      allIds: ['child1', 'child2', 'child3'],
      viewMode: 'auto',
    };

    expect(selectViewMode({ children: state })).toBe('tabs');

    state.allIds.push('child4');
    expect(selectViewMode({ children: state })).toBe('cards');
  });

  it('should toggle child selection', () => {
    const state = {
      ...initialState,
      selectedChildIds: ['child1'],
    };

    const newState = childrenSlice.reducer(
      state,
      toggleChildSelection('child2')
    );

    expect(newState.selectedChildIds).toEqual(['child1', 'child2']);
  });

  it('should persist selected child to localStorage', () => {
    const state = initialState;
    childrenSlice.reducer(state, setSelectedChild('child1'));

    expect(localStorage.getItem('selectedChildId')).toBe('child1');
  });
});

8.1.2 Voice Processing Tests

File: /maternal-app-backend/src/modules/ai/services/voice-processing.service.spec.ts

describe('VoiceProcessingService', () => {
  let service: VoiceProcessingService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [VoiceProcessingService, ...mockProviders],
    }).compile();

    service = module.get<VoiceProcessingService>(VoiceProcessingService);
  });

  it('should detect single child name', async () => {
    const children = [
      { id: 'c1', name: 'Emma' },
      { id: 'c2', name: 'Noah' },
    ];

    const result = service['detectChildNames']('Emma just had a bottle', children);

    expect(result).toEqual([{ id: 'c1', name: 'Emma' }]);
  });

  it('should detect multi-child keywords', async () => {
    const children = [
      { id: 'c1', name: 'Emma' },
      { id: 'c2', name: 'Noah' },
    ];

    const result = service['detectChildNames']('both kids are napping', children);

    expect(result).toEqual(children);
  });

  it('should extract activity type from transcription', () => {
    expect(service['extractActivityType']('Emma just had a bottle')).toBe('feeding');
    expect(service['extractActivityType']('Noah is sleeping')).toBe('sleep');
    expect(service['extractActivityType']('Changed diaper')).toBe('diaper');
  });
});

8.2 Integration Tests

File: /maternal-app-backend/test/tracking-multi-child.e2e-spec.ts

describe('Tracking - Multi-Child (e2e)', () => {
  let app: INestApplication;
  let authToken: string;
  let familyId: string;
  let child1Id: string;
  let child2Id: string;

  beforeAll(async () => {
    // Setup test app, create family with 2 children
  });

  it('should create bulk activities for multiple children', async () => {
    const response = await request(app.getHttpServer())
      .post('/api/v1/tracking/bulk')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        childIds: [child1Id, child2Id],
        type: 'feeding',
        timestamp: new Date().toISOString(),
        details: { method: 'bottle', amount: 180 },
      })
      .expect(201);

    expect(response.body.success).toBe(true);
    expect(response.body.data.count).toBe(2);
    expect(response.body.data.activities).toHaveLength(2);
  });

  it('should fetch activities for multiple children', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/v1/tracking')
      .query({ childIds: `${child1Id},${child2Id}` })
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    const activities = response.body.data;
    const childIds = activities.map(a => a.childId);

    expect(childIds).toContain(child1Id);
    expect(childIds).toContain(child2Id);
  });

  it('should compare children analytics', async () => {
    const response = await request(app.getHttpServer())
      .get('/api/v1/analytics/compare')
      .query({
        childIds: `${child1Id},${child2Id}`,
        metric: 'sleep-patterns',
        startDate: subDays(new Date(), 7).toISOString(),
        endDate: new Date().toISOString(),
      })
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(response.body.data.children).toHaveLength(2);
    expect(response.body.data.metric).toBe('sleep-patterns');
  });
});

8.3 E2E Tests (Frontend)

File: /maternal-web/__tests__/e2e/multi-child-tracking.spec.ts

import { test, expect } from '@playwright/test';

test.describe('Multi-Child Tracking Flow', () => {
  test.beforeEach(async ({ page }) => {
    // Login and navigate to dashboard
    await page.goto('/login');
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'password');
    await page.click('button[type="submit"]');
    await page.waitForURL('/');
  });

  test('should show tabs for 2 children', async ({ page }) => {
    // Assumes test account has 2 children: Emma and Noah
    await expect(page.locator('role=tab[name="Emma"]')).toBeVisible();
    await expect(page.locator('role=tab[name="Noah"]')).toBeVisible();
    await expect(page.locator('role=tab[name="All"]')).toBeVisible();
  });

  test('should log activity for multiple children', async ({ page }) => {
    await page.click('text=Track Feeding');

    // Select both children
    await page.click('[data-testid="child-selector"]');
    await page.click('text=Emma');
    await page.click('text=Noah');

    // Fill form
    await page.selectOption('[name="method"]', 'bottle');
    await page.fill('[name="amount"]', '180');
    await page.click('button:has-text("Save")');

    // Verify success
    await expect(page.locator('text=Activity logged for 2 children')).toBeVisible();
  });

  test('should compare children in analytics', async ({ page }) => {
    await page.click('text=Insights & Analytics');
    await page.click('role=tab[name="Compare"]');

    // Select children
    await page.check('[data-child-id="emma"]');
    await page.check('[data-child-id="noah"]');

    // Select metric
    await page.selectOption('[name="metric"]', 'sleep-patterns');

    // Verify chart shows both children
    await expect(page.locator('.recharts-legend-item:has-text("Emma")')).toBeVisible();
    await expect(page.locator('.recharts-legend-item:has-text("Noah")')).toBeVisible();
  });
});

Migration & Rollout Plan

9.1 Database Migration Sequence

Order of execution:

  1. V008_add_child_display_preferences.sql - Add color/sort columns to children table
  2. V009_create_user_preferences.sql - Create user preferences table
  3. V010_add_activity_constraints.sql - Add bulk operation tracking

Rollback plan:

  • Each migration includes corresponding DOWN migration
  • Test rollbacks in staging environment
  • Keep migrations small and atomic

Pre-migration checklist:

  • Backup production database
  • Test migrations on staging with production data snapshot
  • Verify application works with new schema
  • Monitor query performance after migration

9.2 Feature Flag Strategy

File: /maternal-app-backend/src/common/feature-flags/feature-flags.service.ts

export enum FeatureFlag {
  MULTI_CHILD_COMPARISON = 'multi_child_comparison',
  BULK_ACTIVITY_LOGGING = 'bulk_activity_logging',
  VOICE_MULTI_CHILD = 'voice_multi_child',
  DYNAMIC_DASHBOARD_VIEWS = 'dynamic_dashboard_views',
}

@Injectable()
export class FeatureFlagsService {
  private flags = new Map<FeatureFlag, boolean>([
    [FeatureFlag.MULTI_CHILD_COMPARISON, false],
    [FeatureFlag.BULK_ACTIVITY_LOGGING, false],
    [FeatureFlag.VOICE_MULTI_CHILD, false],
    [FeatureFlag.DYNAMIC_DASHBOARD_VIEWS, false],
  ]);

  isEnabled(flag: FeatureFlag, userId?: string): boolean {
    // Check environment variable override
    const envKey = `FEATURE_${flag.toUpperCase()}`;
    if (process.env[envKey] === 'true') return true;
    if (process.env[envKey] === 'false') return false;

    // Check beta user list
    if (userId && this.isBetaUser(userId)) {
      return true;
    }

    return this.flags.get(flag) || false;
  }

  private isBetaUser(userId: string): boolean {
    const betaUsers = (process.env.BETA_USER_IDS || '').split(',');
    return betaUsers.includes(userId);
  }
}

Frontend feature flag hook:

// /maternal-web/lib/hooks/useFeatureFlag.ts
export const useFeatureFlag = (flag: FeatureFlag): boolean => {
  const { user } = useAuth();
  const [isEnabled, setIsEnabled] = useState(false);

  useEffect(() => {
    // Check with backend
    apiClient.get(`/feature-flags/${flag}`)
      .then(res => setIsEnabled(res.data.enabled))
      .catch(() => setIsEnabled(false));
  }, [flag, user]);

  return isEnabled;
};

// Usage in components:
const showComparison = useFeatureFlag('multi_child_comparison');

9.3 Rollout Phases

Phase 1: Backend Infrastructure (Week 1) COMPLETED

  • Deploy database migrations (V017, V018, V019)
  • Implement bulk activity endpoints (POST /api/v1/activities/bulk)
  • Add comparison endpoints (GET /api/v1/analytics/compare)
  • Update existing endpoints for multi-child filtering (GET /api/v1/activities supports childIds)
  • Add family statistics endpoint (GET /api/v1/children/family/:familyId/statistics)
  • Add child display colors, sort order, and nickname fields
  • Create ComparisonService with 4 metric types (sleep, feeding, diaper, activities)

Phase 2: Frontend Foundation (Week 2) COMPLETED

  • Implement ChildSelector component with single/multiple/all modes
  • Update Redux store for multi-child state (selectedChildIds, viewMode, etc.)
  • Add dynamic dashboard views (tabs for ≤3 children, cards for 4+)
  • Update GraphQL dashboard query to include child display fields
  • Integrate DynamicChildDashboard in home page

Phase 3: Activity Logging (Week 3) IN PROGRESS

  • Update tracking forms with ChildSelector (feeding form complete)
  • Update remaining tracking forms (sleep, diaper, activity, growth, medicine)
  • Implement bulk logging UI in tracking forms
  • Add default child logic and quick select
  • Test and enable for beta users

Note: Pattern established for tracking form updates:

  1. Replace local child state with Redux state
  2. Use ChildSelector component instead of custom selector
  3. Sync selectedChildIds with Redux store
  4. Update API calls to use selectedChild.id

Phase 4: Analytics & Comparison (Week 4) COMPLETED

  • Implement comparison view component with chart visualization
  • Add multi-child charts using Recharts (line charts with color-coded children)
  • Add compareChildren API method with ComparisonMetric enum
  • Support multiple metrics (sleep-patterns, feeding-frequency, diaper-changes, activities)
  • Add date range filtering with DatePicker
  • Show per-child summary cards with metrics

Phase 5: AI & Voice (Week 5)

  • Deploy voice processing updates
  • Implement AI context building
  • Add clarification flows
  • Enable for 75% of users

Phase 6: Full Rollout (Week 6)

  • Enable all features for all users
  • Monitor performance metrics
  • Collect user feedback
  • Address bugs and issues

Performance Optimization

10.1 Database Query Optimization

Indexes for multi-child queries:

-- Composite index for activity filtering
CREATE INDEX idx_activities_child_type_timestamp
ON activities(child_id, type, timestamp DESC);

-- Partial index for recent activities
CREATE INDEX idx_activities_recent
ON activities(child_id, timestamp DESC)
WHERE timestamp > NOW() - INTERVAL '7 days';

-- Index for family-wide queries
CREATE INDEX idx_children_family_active
ON children(family_id, deleted_at)
WHERE deleted_at IS NULL;

Materialized views for aggregations:

-- Daily activity summary per child
CREATE MATERIALIZED VIEW daily_child_activity_summary AS
SELECT
  child_id,
  DATE(timestamp) as activity_date,
  type,
  COUNT(*) as count,
  jsonb_object_agg(detail_key, detail_value) as aggregated_details
FROM activities
WHERE deleted_at IS NULL
GROUP BY child_id, DATE(timestamp), type;

CREATE UNIQUE INDEX idx_daily_summary
ON daily_child_activity_summary(child_id, activity_date, type);

-- Refresh strategy: hourly or on-demand
-- SELECT refresh_materialized_view_concurrently('daily_child_activity_summary');

10.2 Frontend Performance

Code splitting for multi-child features:

// Lazy load comparison view
const ComparisonView = lazy(() =>
  import('@/components/features/analytics/ComparisonView')
);

// Only load when 2+ children exist
{children.length >= 2 && (
  <Suspense fallback={<Skeleton variant="rectangular" height={400} />}>
    <ComparisonView childIds={selectedChildIds} />
  </Suspense>
)}

Memoization for expensive calculations:

// Memoize child color calculation
const getChildColor = useMemo(() => {
  const colorMap = new Map<string, string>();
  children.forEach((child, index) => {
    colorMap.set(child.id, CHILD_COLORS[index % CHILD_COLORS.length]);
  });
  return (childId: string) => colorMap.get(childId) || '#FF6B9D';
}, [children]);

// Memoize filtered activities
const filteredActivities = useMemo(() => {
  if (selectedChildIds.length === 0) return activities;
  return activities.filter(a => selectedChildIds.includes(a.childId));
}, [activities, selectedChildIds]);

Virtual scrolling for many children:

// Use react-window for card view with 5+ children
import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={600}
  itemCount={children.length}
  itemSize={200}
  width="100%"
>
  {({ index, style }) => (
    <div style={style}>
      <ChildCard child={children[index]} />
    </div>
  )}
</FixedSizeList>

10.3 Caching Strategy

Redis caching for analytics:

// Backend caching for comparison results
@Injectable()
export class AnalyticsCacheService {
  constructor(@InjectRedis() private readonly redis: Redis) {}

  async getCachedComparison(
    childIds: string[],
    metric: string,
    dateRange: string,
  ): Promise<ComparisonResult | null> {
    const key = this.buildCacheKey(childIds, metric, dateRange);
    const cached = await this.redis.get(key);
    return cached ? JSON.parse(cached) : null;
  }

  async setCachedComparison(
    childIds: string[],
    metric: string,
    dateRange: string,
    result: ComparisonResult,
  ): Promise<void> {
    const key = this.buildCacheKey(childIds, metric, dateRange);
    // Cache for 5 minutes
    await this.redis.setex(key, 300, JSON.stringify(result));
  }

  private buildCacheKey(
    childIds: string[],
    metric: string,
    dateRange: string,
  ): string {
    const sortedIds = [...childIds].sort().join(',');
    return `analytics:comparison:${sortedIds}:${metric}:${dateRange}`;
  }

  // Invalidate cache when new activity logged
  async invalidateChildCache(childId: string): Promise<void> {
    const pattern = `analytics:comparison:*${childId}*`;
    const keys = await this.redis.keys(pattern);
    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
  }
}

Summary

This comprehensive implementation plan covers:

  1. Frontend: Dynamic UI (tabs/cards), child selector, activity logging, analytics comparison
  2. Backend: Bulk operations, multi-child queries, comparison endpoints, family statistics
  3. AI/Voice: Child name detection, context building, clarification flows, multi-child prompts
  4. Database: Schema enhancements, indexes, materialized views, user preferences
  5. State Management: Redux updates, per-screen selection, comparison preferences
  6. API: Enhanced endpoints, bulk operations, comparison queries
  7. Real-Time: WebSocket child-specific rooms, bulk activity events
  8. Testing: Unit, integration, and E2E tests for multi-child scenarios
  9. Migration: Phased rollout with feature flags and monitoring
  10. Performance: Query optimization, caching, code splitting, memoization

Estimated Timeline: 6 weeks for full implementation and rollout

Key Success Metrics:

  • Zero data leakage between families (security)
  • <200ms query time for multi-child analytics
  • 90%+ user satisfaction with bulk logging
  • 70%+ adoption of comparison features (families with 2+ children)

Next Steps:

  1. Review and approve this plan
  2. Create GitHub issues/tickets for each major component
  3. Set up feature flags in production
  4. Begin Phase 1 (Backend Infrastructure)