# 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](#frontend-implementation)
2. [Backend Implementation](#backend-implementation)
3. [AI & Voice Processing](#ai--voice-processing)
4. [Database Schema Updates](#database-schema-updates)
5. [State Management Architecture](#state-management-architecture)
6. [API Enhancements](#api-enhancements)
7. [Real-Time Sync Updates](#real-time-sync-updates)
8. [Testing Strategy](#testing-strategy)
9. [Migration & Rollout Plan](#migration--rollout-plan)
10. [Performance Optimization](#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:**
```typescript
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:**
```typescript
// Add MUI Tabs component at top of dashboard
{children.map(child => (
}
iconPosition="start"
/>
))}
} iconPosition="start" />
// 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:**
```typescript
// Vertical scrollable card list
{children.map(child => (
{child.name[0]}}
title={child.name}
subheader={`${calculateAge(child.birthDate)} old`}
/>
{/* Compact metrics */}
} value={todaySleepHours} label="Sleep" />
} value={todayFeedings} label="Fed" />
} value={todayDiapers} label="Diapers" />
{/* Last activity timestamp */}
Last activity: {formatDistanceToNow(lastActivity.timestamp)} ago
))}
```
**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)**
```typescript
// Add child selector at top of each tracking form
Who is this for?
{/* Quick select buttons for last active child */}
```
2. **Bulk Logging Support**
```typescript
// When multiple children selected, show batch confirmation
if (selectedChildren.length > 1) {
return (
This will be logged for: {selectedChildren.map(id =>
children.find(c => c.id === id)?.name
).join(', ')}
);
}
// 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`);
};
```
3. **Default Child Logic**
```typescript
// 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:**
```typescript
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):**
```typescript
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
```typescript
// Add tab to analytics page
{children.length >= 2 && }
// Tab panel for comparison
{tabValue === 2 && children.length >= 2 && (
)}
```
---
### 1.5 Recent Activities List Enhancement
**Location:** `/maternal-web/components/features/dashboard/RecentActivities.tsx`
**Updates:**
```typescript
// Add child indicator to each activity item
{child.name[0]}
{activity.type}
{/* Show child name badge */}
}
secondary={formatDistanceToNow(activity.timestamp)}
/>
// Add filter buttons at top
{children.map(child => (
))}
```
---
### 1.6 AI Assistant Updates
**Location:** `/maternal-web/app/ai/page.tsx` and AI components
**Changes:**
1. **Context Awareness**
```typescript
// 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,
})),
};
};
```
2. **Multi-Child Comparison Questions**
```typescript
// 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..."
```
3. **Child Selector in AI Chat**
```typescript
// Add child selector above chat input
Ask about:
setSelectedChildForAI(ids[0])}
showAllOption={true}
compact={true}
/>
```
---
## 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`
```typescript
// Auto-assign consistent colors to children
private readonly CHILD_COLORS = [
'#FF6B9D', '#4ECDC4', '#FFD93D', '#95E1D3',
'#C7CEEA', '#FF8C42', '#A8E6CF', '#B8B8FF',
];
async create(createChildDto: CreateChildDto): Promise {
// 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`
```typescript
@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`
```typescript
@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:**
```typescript
// /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;
@IsOptional()
@IsString()
notes?: string;
}
```
**Service:**
```typescript
// /maternal-app-backend/src/modules/tracking/tracking.service.ts
async createBulkActivities(
dto: CreateBulkActivitiesDto,
userId: string,
): Promise {
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`
```typescript
@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`
```typescript
@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:**
```typescript
// /maternal-app-backend/src/modules/analytics/analytics.service.ts
async compareChildren(params: ComparisonParams): Promise {
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 {
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`
```typescript
async getAggregatedStatistics(
familyId: string,
startDate: Date,
endDate: Date,
): Promise {
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
```typescript
interface VoiceProcessingResult {
transcription: string;
detectedChildIds: string[];
confidence: number;
activityType?: string;
activityDetails?: Record;
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 {
// 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`
```typescript
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
```typescript
interface AIContext {
mode: 'single-child' | 'multi-child' | 'comparison' | 'general';
children: ChildContext[];
selectedChildId?: string;
tokenBudget: number;
priorityWeights: Record;
}
interface ChildContext {
childId: string;
name: string;
age: string;
gender: string;
recentActivities: Activity[];
patterns: PatternInsights;
medicalInfo?: Record;
}
@Injectable()
export class ContextBuilderService {
private readonly TOKEN_BUDGET = 4000;
async buildContext(params: {
childIds?: string[];
mode: AIContext['mode'];
query: string;
}): Promise {
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 {
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 {
// 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`
```typescript
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`
```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`
```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`
```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`
```typescript
interface ChildrenState {
byId: Record;
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) => {
state.selectedChildId = action.payload;
state.selectedChildIds = [action.payload];
// Save to localStorage
if (typeof window !== 'undefined') {
localStorage.setItem('selectedChildId', action.payload);
}
},
setSelectedChildren: (state, action: PayloadAction) => {
state.selectedChildIds = action.payload;
state.selectedChildId = action.payload[0] || null;
},
toggleChildSelection: (state, action: PayloadAction) => {
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) => {
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);
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`
```typescript
interface UIState {
// ... existing UI state
lastSelectedChildPerScreen: Record; // { "/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>
) => {
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`
```typescript
// 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 {
const children = await this.getChildren(familyId);
// Colors now come from backend
return children;
},
};
```
**File:** `/maternal-web/lib/api/tracking.ts`
```typescript
export const trackingApi = {
// ... existing methods
async createBulkActivities(params: {
childIds: string[];
type: string;
timestamp: string;
details?: Record;
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`
```typescript
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`
```typescript
@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);
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`
```typescript
export class WebSocketClient {
private socket: Socket | null = null;
private subscribedChildIds: Set = 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`
```typescript
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`
```typescript
describe('VoiceProcessingService', () => {
let service: VoiceProcessingService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [VoiceProcessingService, ...mockProviders],
}).compile();
service = module.get(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`
```typescript
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`
```typescript
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`
```typescript
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.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:**
```typescript
// /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
- [x] Deploy database migrations (V017, V018, V019)
- [x] Implement bulk activity endpoints (POST /api/v1/activities/bulk)
- [x] Add comparison endpoints (GET /api/v1/analytics/compare)
- [x] Update existing endpoints for multi-child filtering (GET /api/v1/activities supports childIds)
- [x] Add family statistics endpoint (GET /api/v1/children/family/:familyId/statistics)
- [x] Add child display colors, sort order, and nickname fields
- [x] Create ComparisonService with 4 metric types (sleep, feeding, diaper, activities)
#### Phase 2: Frontend Foundation (Week 2) ✅ COMPLETED
- [x] Implement ChildSelector component with single/multiple/all modes
- [x] Update Redux store for multi-child state (selectedChildIds, viewMode, etc.)
- [x] Add dynamic dashboard views (tabs for ≤3 children, cards for 4+)
- [x] Update GraphQL dashboard query to include child display fields
- [x] Integrate DynamicChildDashboard in home page
#### Phase 3: Activity Logging (Week 3) ⏳ IN PROGRESS
- [x] 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
- [x] Implement comparison view component with chart visualization
- [x] Add multi-child charts using Recharts (line charts with color-coded children)
- [x] Add compareChildren API method with ComparisonMetric enum
- [x] Support multiple metrics (sleep-patterns, feeding-frequency, diaper-changes, activities)
- [x] Add date range filtering with DatePicker
- [x] 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:**
```sql
-- 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:**
```sql
-- 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:**
```typescript
// Lazy load comparison view
const ComparisonView = lazy(() =>
import('@/components/features/analytics/ComparisonView')
);
// Only load when 2+ children exist
{children.length >= 2 && (
}>
)}
```
**Memoization for expensive calculations:**
```typescript
// Memoize child color calculation
const getChildColor = useMemo(() => {
const colorMap = new Map();
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:**
```typescript
// Use react-window for card view with 5+ children
import { FixedSizeList } from 'react-window';
{({ index, style }) => (
)}
```
---
### 10.3 Caching Strategy
**Redis caching for analytics:**
```typescript
// Backend caching for comparison results
@Injectable()
export class AnalyticsCacheService {
constructor(@InjectRedis() private readonly redis: Redis) {}
async getCachedComparison(
childIds: string[],
metric: string,
dateRange: string,
): Promise {
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 {
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 {
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)