From 95ef0e5e785db841397da612430f6b45191e9bab Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Oct 2025 21:05:14 +0000 Subject: [PATCH] docs: Add comprehensive multi-child implementation plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added detailed implementation plan covering: - Frontend: Dynamic UI, child selector, bulk activity logging, comparison analytics - Backend: Bulk operations, multi-child queries, family statistics - AI/Voice: Child name detection, context building, clarification flows - Database: Schema enhancements, user preferences, bulk operation tracking - State management, API enhancements, real-time sync updates - Testing strategy: Unit, integration, and E2E tests - Migration plan with feature flags for phased rollout - Performance optimizations: Caching, indexes, code splitting Also includes: - Security fixes for multi-family data leakage in analytics pages - ParentFlow branding updates - Activity tracking navigation improvements - Backend DTO and error handling fixes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../multi-child-implementation.md | 244 ++ docs/multi-child-implementation-plan.md | 2274 +++++++++++++++++ .../modules/children/children.controller.ts | 13 +- .../modules/children/dto/create-child.dto.ts | 4 + maternal-web/app/(auth)/onboarding/page.tsx | 84 +- maternal-web/app/activities/page.tsx | 276 -- maternal-web/app/analytics/page.tsx | 53 +- maternal-web/app/page.tsx | 2 +- .../features/analytics/InsightsDashboard.tsx | 39 +- .../analytics/UnifiedInsightsDashboard.tsx | 62 +- .../components/layouts/AppShell/AppShell.tsx | 46 +- maternal-web/public/sw.js | 2 +- 12 files changed, 2778 insertions(+), 321 deletions(-) create mode 100644 docs/implementation-docs/multi-child-implementation.md create mode 100644 docs/multi-child-implementation-plan.md delete mode 100644 maternal-web/app/activities/page.tsx diff --git a/docs/implementation-docs/multi-child-implementation.md b/docs/implementation-docs/multi-child-implementation.md new file mode 100644 index 0000000..6f33196 --- /dev/null +++ b/docs/implementation-docs/multi-child-implementation.md @@ -0,0 +1,244 @@ +# Multi-Child Support Implementation Guide + +## Overview +Implementation specifications for supporting multiple children in the maternal organization app with dynamic UI adaptation based on the number of children registered. + +--- + +## Core Rules Implementation + +### Rule 1: Dynamic Dashboard View (1-3 vs 3+ children) + +**For 1-3 Children - Tab Implementation:** +- Display horizontal tabs at the top of the dashboard +- Each tab shows one child's name +- Add an "All" tab at the end to view combined summary +- Active tab should be visually distinct (use primary color from Design System) +- Tab transitions should be smooth and instantaneous +- Selected tab state must persist during app session + +**For 3+ Children - Stacked Cards Implementation:** +- Display vertically scrollable cards +- Each card represents one child +- Cards should show compact summary (key metrics only) +- Include child's name and age on each card +- Add visual separation between cards (8px spacing) +- Implement pull-to-refresh for updating all cards + +### Rule 2: Global Child Selector + +**Implementation Requirements:** +- Add dropdown selector to app header (below navigation bar) +- Only display when family has 2+ children +- Show currently selected child(ren) name(s) +- Dropdown options must include: + - Individual child names with profile photos (if available) + - "All Children" option + - Visual indicator for currently selected +- Selector state must persist across all screens +- Update all displayed data when selection changes + +--- + +## Screen-by-Screen Updates + +### Dashboard Screen +- Implement view mode detection based on children count +- Add child selector if 2+ children +- Update summary calculations based on selected child(ren) +- For "All" selection, show aggregated statistics +- Ensure real-time sync updates correct child's data + +### Activity Logging Screens +- Add child selection step before logging +- Default to last active child +- Allow bulk logging for multiple children (e.g., "both kids napping") +- Update confirmation messages to include child name(s) +- Store child association with each activity + +### AI Assistant Screen +- Modify context building to include selected child's data +- Update prompts to specify which child when asking questions +- Allow questions about multiple children comparisons +- Include child name in AI responses for clarity + +### Analytics Screen +- Add "Comparison" tab as specified in Rule 4 +- Update all charts to filter by selected child(ren) +- Implement side-by-side view for comparing 2 children +- Add legend when showing multiple children's data +- Export reports should specify which child(ren) included + +--- + +## Rule 3: Analytics Comparison Tab + +### Comparison View Requirements +- Add new tab in Analytics section labeled "Compare" +- Only show when 2+ children registered +- Default to comparing all children +- Implement following comparison metrics: + - Sleep patterns overlay chart + - Feeding frequency comparison + - Growth curves on same graph + - Development milestones timeline + - Activity patterns heatmap + +### Comparison Controls +- Add date range selector (default: last 7 days) +- Include child selector checkboxes +- Implement metric selector (choose what to compare) +- Add export functionality for comparison reports +- Use different colors per child (from a predefined palette) + +--- + +## Rule 4: Voice Command Multi-Child Handling + +### Voice Processing Updates + +**Child Detection Logic:** +1. Parse voice input for child names +2. If child name detected, associate with that child +3. If no child name detected: + - For single child families: auto-select + - For multi-child families: prompt for selection +4. Support keywords: "both", "all kids", "everyone" + +**Clarification Prompts:** +- "Which child is this for?" +- "Did you mean [Child Name]?" +- "Should I log this for all children?" +- Display child selector with voice feedback +- Allow voice response for selection + +**Natural Language Patterns to Support:** +- "[Child name] just [activity]" +- "Both kids are [activity]" +- "Log feeding for [child name]" +- "All children went to bed" +- "The twins are napping" + +--- + +## State Management Updates + +### Redux Store Modifications + +**UI Slice Updates:** +- Add active children IDs array (supports multi-select) +- Store default child ID for quick actions +- Add view mode preference (tabs vs cards) +- Cache last selected child per screen +- Store comparison view preferences + +**Activities Slice Updates:** +- Index activities by child ID +- Support filtering by multiple children +- Add bulk operation support +- Update sync to handle multi-child selections + +**Analytics Slice Updates:** +- Cache calculations per child +- Store comparison configurations +- Add aggregation logic for "all children" view + +--- + +## API Updates + +### Endpoint Modifications + +**Activity Endpoints:** +- Accept child_id array for bulk operations +- Return child-specific data in responses +- Support filtering by multiple children +- Add comparison-specific endpoints + +**Analytics Endpoints:** +- Add comparison query parameters +- Support multi-child aggregations +- Return child-separated statistics +- Include child identifiers in all responses + +### WebSocket Updates +- Include child ID in all real-time events +- Support room-per-child architecture +- Emit updates only for selected children +- Handle bulk activity broadcasts + +--- + +## Database Updates + +### Schema Modifications +- Ensure all activity tables include child_id foreign key +- Add indexes on child_id for performance +- Create views for multi-child aggregations +- Add comparison preferences table + +### Query Optimizations +- Implement efficient multi-child queries +- Add database functions for aggregations +- Optimize for common comparison queries +- Cache frequently accessed combinations + +--- + +## Testing Requirements + +### Unit Tests +- Test view mode selection logic +- Verify child selector state management +- Test voice command child detection +- Validate aggregation calculations + +### Integration Tests +- Test multi-child activity logging flow +- Verify real-time sync for multiple children +- Test comparison view data accuracy +- Validate API filtering with multiple children + +### E2E Tests +- Complete flow for 2-child family +- Complete flow for 4+ child family +- Voice command with child selection +- Analytics comparison interactions + +--- + +## Migration Plan + +### For Existing Users +1. Detect single-child accounts +2. Hide multi-child features initially +3. Enable features when second child added +4. Preserve all existing data associations +5. Default existing activities to first child + +### UI Migration +1. Add child selector component globally +2. Update dashboard based on child count +3. Add comparison tab to analytics +4. Update all logging flows +5. Modify voice processing pipeline + +--- + +## Performance Considerations + +- Lazy load child data as needed +- Cache per-child calculations +- Implement virtual scrolling for many children +- Optimize database queries for multi-child filters +- Throttle real-time updates when showing multiple children + +--- + +## Accessibility Updates + +- Ensure child selector is screen-reader friendly +- Add voice announcements for child selection +- Label all charts with child names +- Support keyboard navigation for tabs +- Provide text alternatives for comparison visualizations \ No newline at end of file diff --git a/docs/multi-child-implementation-plan.md b/docs/multi-child-implementation-plan.md new file mode 100644 index 0000000..3469ac8 --- /dev/null +++ b/docs/multi-child-implementation-plan.md @@ -0,0 +1,2274 @@ +# 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) +- [ ] Deploy database migrations +- [ ] Implement bulk activity endpoints +- [ ] Add comparison endpoints +- [ ] Update existing endpoints for multi-child filtering +- [ ] Deploy with feature flags disabled + +#### Phase 2: Frontend Foundation (Week 2) +- [ ] Implement ChildSelector component +- [ ] Update Redux store for multi-child state +- [ ] Add dynamic dashboard views (tabs/cards) +- [ ] Test with feature flag enabled for beta users + +#### Phase 3: Activity Logging (Week 3) +- [ ] Update all tracking forms with child selection +- [ ] Implement bulk logging UI +- [ ] Add default child logic +- [ ] Enable for 25% of users + +#### Phase 4: Analytics & Comparison (Week 4) +- [ ] Implement comparison tab +- [ ] Add multi-child charts +- [ ] Update existing analytics pages +- [ ] Enable for 50% of users + +#### 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) diff --git a/maternal-app/maternal-app-backend/src/modules/children/children.controller.ts b/maternal-app/maternal-app-backend/src/modules/children/children.controller.ts index dc2f90c..4969a6b 100644 --- a/maternal-app/maternal-app-backend/src/modules/children/children.controller.ts +++ b/maternal-app/maternal-app-backend/src/modules/children/children.controller.ts @@ -10,6 +10,7 @@ import { HttpCode, HttpStatus, Query, + BadRequestException, } from '@nestjs/common'; import { ChildrenService } from './children.service'; import { CreateChildDto } from './dto/create-child.dto'; @@ -30,13 +31,7 @@ export class ChildrenController { @Body() createChildDto: CreateChildDto, ) { if (!familyId) { - return { - success: false, - error: { - code: 'VALIDATION_ERROR', - message: 'familyId query parameter is required', - }, - }; + throw new BadRequestException('familyId query parameter is required'); } const child = await this.childrenService.create( @@ -55,6 +50,7 @@ export class ChildrenController { birthDate: child.birthDate, gender: child.gender, photoUrl: child.photoUrl, + photoAlt: child.photoAlt, medicalInfo: child.medicalInfo, createdAt: child.createdAt, }, @@ -85,6 +81,7 @@ export class ChildrenController { birthDate: child.birthDate, gender: child.gender, photoUrl: child.photoUrl, + photoAlt: child.photoAlt, medicalInfo: child.medicalInfo, createdAt: child.createdAt, })), @@ -107,6 +104,7 @@ export class ChildrenController { birthDate: child.birthDate, gender: child.gender, photoUrl: child.photoUrl, + photoAlt: child.photoAlt, medicalInfo: child.medicalInfo, createdAt: child.createdAt, }, @@ -152,6 +150,7 @@ export class ChildrenController { birthDate: child.birthDate, gender: child.gender, photoUrl: child.photoUrl, + photoAlt: child.photoAlt, medicalInfo: child.medicalInfo, createdAt: child.createdAt, }, diff --git a/maternal-app/maternal-app-backend/src/modules/children/dto/create-child.dto.ts b/maternal-app/maternal-app-backend/src/modules/children/dto/create-child.dto.ts index e056139..d61c71d 100644 --- a/maternal-app/maternal-app-backend/src/modules/children/dto/create-child.dto.ts +++ b/maternal-app/maternal-app-backend/src/modules/children/dto/create-child.dto.ts @@ -32,6 +32,10 @@ export class CreateChildDto { @IsString() photoUrl?: string; + @IsOptional() + @IsString() + photoAlt?: string; + @IsOptional() @IsObject() medicalInfo?: Record; diff --git a/maternal-web/app/(auth)/onboarding/page.tsx b/maternal-web/app/(auth)/onboarding/page.tsx index 0c0593c..eca0458 100644 --- a/maternal-web/app/(auth)/onboarding/page.tsx +++ b/maternal-web/app/(auth)/onboarding/page.tsx @@ -23,6 +23,9 @@ import { FormControlLabel, FormControl, Grid, + StepConnector, + stepConnectorClasses, + styled, } from '@mui/material'; import { ArrowBack, ArrowForward, Check, Language, Straighten } from '@mui/icons-material'; import { motion, AnimatePresence } from 'framer-motion'; @@ -33,9 +36,60 @@ import { useLocale, MeasurementSystem } from '@/hooks/useLocale'; import { useTranslation } from '@/hooks/useTranslation'; import { supportedLanguages } from '@/lib/i18n/config'; import { usersApi } from '@/lib/api/users'; +import { useTheme } from '@mui/material/styles'; +import { StepIconProps } from '@mui/material/StepIcon'; const steps = ['Welcome', 'Language', 'Measurements', 'Add Child', 'Complete']; +// Custom connector for mobile-friendly stepper +const CustomConnector = styled(StepConnector)(({ theme }) => ({ + [`&.${stepConnectorClasses.active}`]: { + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.primary.main, + }, + }, + [`&.${stepConnectorClasses.completed}`]: { + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.primary.main, + }, + }, + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.divider, + borderTopWidth: 2, + borderRadius: 1, + }, +})); + +// Custom step icon showing numbers +const CustomStepIconRoot = styled('div')<{ ownerState: { active?: boolean; completed?: boolean } }>( + ({ theme, ownerState }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 32, + height: 32, + borderRadius: '50%', + backgroundColor: ownerState.completed + ? theme.palette.primary.main + : ownerState.active + ? theme.palette.primary.main + : theme.palette.grey[300], + color: ownerState.active || ownerState.completed ? '#fff' : theme.palette.text.secondary, + fontWeight: 600, + fontSize: '0.875rem', + }) +); + +function CustomStepIcon(props: StepIconProps) { + const { active, completed, icon } = props; + + return ( + + {completed ? : icon} + + ); +} + export default function OnboardingPage() { const [activeStep, setActiveStep] = useState(0); const [selectedLanguage, setSelectedLanguage] = useState('en'); @@ -49,6 +103,7 @@ export default function OnboardingPage() { const { user, refreshUser } = useAuth(); const { setLanguage, setMeasurementSystem } = useLocale(); const { t } = useTranslation('onboarding'); + const theme = useTheme(); const handleNext = async () => { setError(''); @@ -154,7 +209,7 @@ export default function OnboardingPage() { flexDirection: 'column', px: 3, py: 4, - background: 'linear-gradient(135deg, #FFE4E1 0%, #FFDAB9 100%)', + background: `linear-gradient(135deg, ${theme.palette.primary.light} 0%, ${theme.palette.secondary.light} 100%)`, }} > - + } + sx={{ mb: 4 }} + > {steps.map((label, index) => { let stepLabel = label; if (index === 0) stepLabel = t('welcome.title').split('!')[0]; @@ -177,10 +237,24 @@ export default function OnboardingPage() { else if (index === 2) stepLabel = t('measurements.title'); else if (index === 3) stepLabel = t('child.title'); else if (index === 4) stepLabel = t('complete.title').split('!')[0]; - + + // Only show label for active step on mobile + const showLabel = activeStep === index; + return ( - {stepLabel} + + {stepLabel} + ); })} diff --git a/maternal-web/app/activities/page.tsx b/maternal-web/app/activities/page.tsx deleted file mode 100644 index 72c8755..0000000 --- a/maternal-web/app/activities/page.tsx +++ /dev/null @@ -1,276 +0,0 @@ -'use client'; - -import { useState, useEffect, useCallback } from 'react'; -import { - Box, - Typography, - Paper, - List, - ListItem, - ListItemIcon, - ListItemText, - Chip, - CircularProgress, - Snackbar, - Alert, -} from '@mui/material'; -import { - Restaurant, - Hotel, - BabyChangingStation, - MedicalServices, - EmojiEvents, - Note, -} from '@mui/icons-material'; -import { AppShell } from '@/components/layouts/AppShell/AppShell'; -import { ProtectedRoute } from '@/components/common/ProtectedRoute'; -import { useAuth } from '@/lib/auth/AuthContext'; -import { childrenApi, Child } from '@/lib/api/children'; -import { trackingApi, Activity } from '@/lib/api/tracking'; -import { useLocalizedDate } from '@/hooks/useLocalizedDate'; -import { useRealTimeActivities } from '@/hooks/useWebSocket'; - -const activityIcons: Record = { - feeding: , - sleep: , - diaper: , - medication: , - milestone: , - note: , -}; - -const activityColors: Record = { - feeding: '#FFB6C1', - sleep: '#B6D7FF', - diaper: '#FFE4B5', - medication: '#FFB8B8', - milestone: '#FFD700', - note: '#E0E0E0', -}; - -export default function ActivitiesPage() { - const { user } = useAuth(); - const { format } = useLocalizedDate(); - const [children, setChildren] = useState([]); - const [selectedChild, setSelectedChild] = useState(null); - const [activities, setActivities] = useState([]); - const [loading, setLoading] = useState(true); - const [notification, setNotification] = useState(null); - - const familyId = user?.families?.[0]?.familyId; - - // Real-time activity handlers - const handleActivityCreated = useCallback((activity: Activity) => { - console.log('[ActivitiesPage] Real-time activity created:', activity); - setActivities((prev) => [activity, ...prev]); - setNotification('New activity added by family member'); - }, []); - - const handleActivityUpdated = useCallback((activity: Activity) => { - console.log('[ActivitiesPage] Real-time activity updated:', activity); - setActivities((prev) => - prev.map((a) => (a.id === activity.id ? activity : a)) - ); - setNotification('Activity updated by family member'); - }, []); - - const handleActivityDeleted = useCallback((data: { activityId: string }) => { - console.log('[ActivitiesPage] Real-time activity deleted:', data); - setActivities((prev) => prev.filter((a) => a.id !== data.activityId)); - setNotification('Activity deleted by family member'); - }, []); - - // Subscribe to real-time updates - useRealTimeActivities( - handleActivityCreated, - handleActivityUpdated, - handleActivityDeleted - ); - - useEffect(() => { - const loadData = async () => { - if (!familyId) { - setLoading(false); - return; - } - - try { - const childrenData = await childrenApi.getChildren(familyId); - setChildren(childrenData); - - if (childrenData.length > 0) { - const firstChild = childrenData[0]; - setSelectedChild(firstChild); - - // Load activities for the last 7 days - const endDate = format(new Date(), 'yyyy-MM-dd'); - const startDate = format( - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - 'yyyy-MM-dd' - ); - - const activitiesData = await trackingApi.getActivities( - firstChild.id, - undefined, - startDate, - endDate - ); - setActivities(activitiesData); - } - } catch (error) { - console.error('[ActivitiesPage] Failed to load data:', error); - } finally { - setLoading(false); - } - }; - - loadData(); - }, [familyId]); - - const formatActivityTime = (timestamp: string) => { - const date = new Date(timestamp); - const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); - - const isToday = date.toDateString() === today.toDateString(); - const isYesterday = date.toDateString() === yesterday.toDateString(); - - if (isToday) { - return `Today at ${format(date, 'h:mm a')}`; - } else if (isYesterday) { - return `Yesterday at ${format(date, 'h:mm a')}`; - } else { - return format(date, 'MMM d, h:mm a'); - } - }; - - const getActivityDescription = (activity: Activity) => { - switch (activity.type) { - case 'feeding': - return activity.data?.amount - ? `${activity.data.amount} ${activity.data.unit || 'oz'}` - : 'Feeding'; - case 'sleep': - if (activity.data?.endedAt) { - const duration = Math.floor( - (new Date(activity.data.endedAt).getTime() - - new Date(activity.timestamp).getTime()) / - 60000 - ); - const hours = Math.floor(duration / 60); - const mins = duration % 60; - return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; - } - return 'Sleep'; - case 'diaper': - return activity.data?.type || 'Diaper change'; - case 'medication': - return activity.data?.name || 'Medication'; - default: - return activity.type; - } - }; - - return ( - - - - - Recent Activities - - - {loading ? ( - - - - ) : activities.length === 0 ? ( - - - No activities recorded yet - - - ) : ( - - - {activities.map((activity, index) => ( - - - - {activityIcons[activity.type] || } - - - - - {getActivityDescription(activity)} - - - - } - secondary={ - - - {formatActivityTime(activity.timestamp)} - - {activity.notes && ( - - {activity.notes} - - )} - - } - /> - - ))} - - - )} - - - {/* Real-time update notification */} - setNotification(null)} - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} - > - setNotification(null)} - severity="info" - sx={{ width: '100%' }} - > - {notification} - - - - - ); -} diff --git a/maternal-web/app/analytics/page.tsx b/maternal-web/app/analytics/page.tsx index 89ac95e..5e3be95 100644 --- a/maternal-web/app/analytics/page.tsx +++ b/maternal-web/app/analytics/page.tsx @@ -40,6 +40,7 @@ import PredictionsCard from '@/components/features/analytics/PredictionsCard'; import GrowthSpurtAlert from '@/components/features/analytics/GrowthSpurtAlert'; import WeeklyReportCard from '@/components/features/analytics/WeeklyReportCard'; import MonthlyReportCard from '@/components/features/analytics/MonthlyReportCard'; +import { useAuth } from '@/lib/auth/AuthContext'; interface TabPanelProps { children?: React.ReactNode; @@ -65,6 +66,7 @@ function TabPanel(props: TabPanelProps) { export default function AnalyticsPage() { const theme = useTheme(); + const { user } = useAuth(); const [children, setChildren] = useState([]); const [selectedChildId, setSelectedChildId] = useState(''); const [tabValue, setTabValue] = useState(0); @@ -74,27 +76,54 @@ export default function AnalyticsPage() { const [insightsLoading, setInsightsLoading] = useState(false); const [predictionsLoading, setPredictionsLoading] = useState(false); const [days, setDays] = useState(7); + const [error, setError] = useState(''); + + const familyId = user?.families?.[0]?.familyId; useEffect(() => { - loadChildren(); - }, []); - - useEffect(() => { - if (selectedChildId) { - loadInsights(); - loadPredictions(); + if (familyId) { + loadChildren(); } - }, [selectedChildId, days]); + }, [familyId]); + + useEffect(() => { + if (selectedChildId && children.length > 0) { + // Validate that selectedChildId belongs to current user's children + const childExists = children.some(child => child.id === selectedChildId); + if (childExists) { + loadInsights(); + loadPredictions(); + } else { + console.warn('[AnalyticsPage] Selected child not found in user\'s children, resetting'); + setSelectedChildId(children[0].id); + setError('Selected child not found. Showing data for your first child.'); + } + } + }, [selectedChildId, days, children]); const loadChildren = async () => { + if (!familyId) { + setLoading(false); + setError('No family found'); + return; + } + try { - const data = await childrenApi.getChildren(); + console.log('[AnalyticsPage] Loading children for familyId:', familyId); + const data = await childrenApi.getChildren(familyId); + console.log('[AnalyticsPage] Loaded children:', data); setChildren(data); - if (data.length > 0 && !selectedChildId) { - setSelectedChildId(data[0].id); + + if (data.length > 0) { + const existingChildStillValid = data.some(child => child.id === selectedChildId); + if (!selectedChildId || !existingChildStillValid) { + setSelectedChildId(data[0].id); + } } + setError(''); } catch (error) { - console.error('Failed to load children:', error); + console.error('[AnalyticsPage] Failed to load children:', error); + setError('Failed to load children'); } finally { setLoading(false); } diff --git a/maternal-web/app/page.tsx b/maternal-web/app/page.tsx index bf0ecae..a899516 100644 --- a/maternal-web/app/page.tsx +++ b/maternal-web/app/page.tsx @@ -124,7 +124,7 @@ export default function HomePage() { { icon: , label: t('quickActions.sleep'), color: theme.palette.secondary.main, path: '/track/sleep' }, { icon: , label: t('quickActions.diaper'), color: theme.palette.warning.main, path: '/track/diaper' }, { icon: , label: t('quickActions.medical'), color: theme.palette.error.main, path: '/track/medicine' }, - { icon: , label: t('quickActions.activities'), color: theme.palette.success.main, path: '/activities' }, + { icon: , label: t('quickActions.activities'), color: theme.palette.success.main, path: '/track/activity' }, { icon: , label: t('quickActions.aiAssistant'), color: theme.palette.info.main, path: '/ai-assistant' }, ]; diff --git a/maternal-web/components/features/analytics/InsightsDashboard.tsx b/maternal-web/components/features/analytics/InsightsDashboard.tsx index add5faf..1eac788 100644 --- a/maternal-web/components/features/analytics/InsightsDashboard.tsx +++ b/maternal-web/components/features/analytics/InsightsDashboard.tsx @@ -44,6 +44,7 @@ import { useLocalizedDate } from '@/hooks/useLocalizedDate'; import { useTranslation } from '@/hooks/useTranslation'; import { useFormatting } from '@/hooks/useFormatting'; import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { useAuth } from '@/lib/auth/AuthContext'; type DateRange = '7days' | '30days' | '3months'; @@ -100,6 +101,7 @@ const getActivityColor = (type: ActivityType) => { export const InsightsDashboard: React.FC = () => { const router = useRouter(); + const { user } = useAuth(); const { format, formatDistanceToNow } = useLocalizedDate(); const { t } = useTranslation('insights'); const { formatNumber } = useFormatting(); @@ -110,30 +112,55 @@ export const InsightsDashboard: React.FC = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const familyId = user?.families?.[0]?.familyId; + // Fetch children on mount useEffect(() => { const fetchChildren = async () => { + if (!familyId) { + setError('No family found'); + return; + } + try { - const childrenData = await childrenApi.getChildren(); + console.log('[InsightsDashboard] Loading children for familyId:', familyId); + const childrenData = await childrenApi.getChildren(familyId); + console.log('[InsightsDashboard] Loaded children:', childrenData); setChildren(childrenData); + if (childrenData.length > 0) { - setSelectedChild(childrenData[0].id); + // Validate selected child or pick first one + const validChild = childrenData.find(c => c.id === selectedChild); + if (!validChild) { + setSelectedChild(childrenData[0].id); + } } } catch (err: any) { + console.error('[InsightsDashboard] Failed to load children:', err); setError(err.response?.data?.message || t('errors.loadChildren')); } }; fetchChildren(); - }, []); + }, [familyId]); // Fetch activities when child or date range changes useEffect(() => { - if (!selectedChild) return; + if (!selectedChild || children.length === 0) return; + + // Validate that selectedChild belongs to current user's children + const childExists = children.some(child => child.id === selectedChild); + if (!childExists) { + console.warn('[InsightsDashboard] Selected child not found in user\'s children, resetting'); + setSelectedChild(children[0].id); + setError('Selected child not found. Showing data for your first child.'); + return; + } const fetchActivities = async () => { setLoading(true); setError(null); try { + console.log('[InsightsDashboard] Fetching activities for child:', selectedChild); const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90; const endDate = endOfDay(new Date()); const startDate = startOfDay(subDays(new Date(), days - 1)); @@ -144,8 +171,10 @@ export const InsightsDashboard: React.FC = () => { startDate.toISOString(), endDate.toISOString() ); + console.log('[InsightsDashboard] Fetched activities:', activitiesData.length); setActivities(activitiesData); } catch (err: any) { + console.error('[InsightsDashboard] Failed to load activities:', err); setError(err.response?.data?.message || t('errors.loadActivities')); } finally { setLoading(false); @@ -153,7 +182,7 @@ export const InsightsDashboard: React.FC = () => { }; fetchActivities(); - }, [selectedChild, dateRange]); + }, [selectedChild, dateRange, children]); // Calculate statistics const calculateStats = () => { diff --git a/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx b/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx index d62f75e..28f3047 100644 --- a/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx +++ b/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx @@ -21,6 +21,7 @@ import { InsightsDashboard } from './InsightsDashboard'; import PredictionsCard from './PredictionsCard'; import GrowthSpurtAlert from './GrowthSpurtAlert'; import { motion } from 'framer-motion'; +import { useAuth } from '@/lib/auth/AuthContext'; interface TabPanelProps { children?: React.ReactNode; @@ -45,6 +46,7 @@ function TabPanel(props: TabPanelProps) { } export function UnifiedInsightsDashboard() { + const { user } = useAuth(); const [children, setChildren] = useState([]); const [selectedChildId, setSelectedChildId] = useState(''); const [tabValue, setTabValue] = useState(0); @@ -54,27 +56,56 @@ export function UnifiedInsightsDashboard() { const [insightsLoading, setInsightsLoading] = useState(false); const [predictionsLoading, setPredictionsLoading] = useState(false); const [days, setDays] = useState(7); + const [error, setError] = useState(''); + + const familyId = user?.families?.[0]?.familyId; useEffect(() => { - loadChildren(); - }, []); - - useEffect(() => { - if (selectedChildId) { - loadInsights(); - loadPredictions(); + if (familyId) { + loadChildren(); } - }, [selectedChildId, days]); + }, [familyId]); + + useEffect(() => { + if (selectedChildId && children.length > 0) { + // Validate that selectedChildId belongs to current user's children + const childExists = children.some(child => child.id === selectedChildId); + if (childExists) { + loadInsights(); + loadPredictions(); + } else { + // Invalid child ID - reset to first child + console.warn('[UnifiedInsightsDashboard] Selected child not found in user\'s children, resetting'); + setSelectedChildId(children[0].id); + setError('Selected child not found. Showing data for your first child.'); + } + } + }, [selectedChildId, days, children]); const loadChildren = async () => { + if (!familyId) { + setLoading(false); + setError('No family found'); + return; + } + try { - const data = await childrenApi.getChildren(); + console.log('[UnifiedInsightsDashboard] Loading children for familyId:', familyId); + const data = await childrenApi.getChildren(familyId); + console.log('[UnifiedInsightsDashboard] Loaded children:', data); setChildren(data); - if (data.length > 0 && !selectedChildId) { - setSelectedChildId(data[0].id); + + // Only set selectedChildId if we don't have one or if it's not in the new list + if (data.length > 0) { + const existingChildStillValid = data.some(child => child.id === selectedChildId); + if (!selectedChildId || !existingChildStillValid) { + setSelectedChildId(data[0].id); + } } + setError(''); } catch (error) { - console.error('Failed to load children:', error); + console.error('[UnifiedInsightsDashboard] Failed to load children:', error); + setError('Failed to load children'); } finally { setLoading(false); } @@ -141,6 +172,13 @@ export function UnifiedInsightsDashboard() { + {/* Error Alert */} + {error && ( + setError('')}> + {error} + + )} + {/* Child Selector */} {children.length > 1 && ( diff --git a/maternal-web/components/layouts/AppShell/AppShell.tsx b/maternal-web/components/layouts/AppShell/AppShell.tsx index b1ba79e..3607f98 100644 --- a/maternal-web/components/layouts/AppShell/AppShell.tsx +++ b/maternal-web/components/layouts/AppShell/AppShell.tsx @@ -19,10 +19,11 @@ import { TabBar } from '../TabBar/TabBar'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { ReactNode } from 'react'; import { useWebSocket } from '@/hooks/useWebSocket'; -import { Wifi, WifiOff, People, AccountCircle, Settings, ChildCare, Group, Logout, Gavel } from '@mui/icons-material'; +import { Wifi, WifiOff, People, AccountCircle, Settings, ChildCare, Group, Logout, Gavel, Favorite } from '@mui/icons-material'; import { useTranslation } from '@/hooks/useTranslation'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/lib/auth/AuthContext'; +import Link from 'next/link'; interface AppShellProps { children: ReactNode; @@ -93,7 +94,7 @@ export const AppShell = ({ children }: AppShellProps) => { }} > {/* Left Side - Family Members Online Indicator */} - + {isConnected && presence.count > 1 && ( { )} + {/* Center - Logo */} + + + `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`, + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + }} + > + ParentFlow + + + {/* Right Side - User Menu Button with Status Indicator */} + { {t('navigation.logout')} + (a=new URL(a+".js",c).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(c,i)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(s[n])return;let t={};const r=e=>a(e,n),d={module:{uri:n},exports:t,require:r};s[n]=Promise.all(c.map(e=>d[e]||r(e))).then(e=>(i(...e),t))}}define(["./workbox-4d767a27"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"9753bde17273dc96d4d9d00785e27b72"},{url:"/_next/static/chunks/101-3dd0627909cd6c22.js",revision:"3dd0627909cd6c22"},{url:"/_next/static/chunks/1213-7820689c8a23df1d.js",revision:"7820689c8a23df1d"},{url:"/_next/static/chunks/1233-aa8672e107c5a9d6.js",revision:"aa8672e107c5a9d6"},{url:"/_next/static/chunks/1255-b2f7fd83e387a9e1.js",revision:"b2f7fd83e387a9e1"},{url:"/_next/static/chunks/1280-296e0a2b6e9dd9b1.js",revision:"296e0a2b6e9dd9b1"},{url:"/_next/static/chunks/1543-530e0f57f7af68aa.js",revision:"530e0f57f7af68aa"},{url:"/_next/static/chunks/1863-7231108310f72246.js",revision:"7231108310f72246"},{url:"/_next/static/chunks/2262-26293d6453fcc927.js",revision:"26293d6453fcc927"},{url:"/_next/static/chunks/2349-f488a1827d114358.js",revision:"f488a1827d114358"},{url:"/_next/static/chunks/2449-b8a41aa6a7d2d3a4.js",revision:"b8a41aa6a7d2d3a4"},{url:"/_next/static/chunks/2619-04bc32f026a0d946.js",revision:"04bc32f026a0d946"},{url:"/_next/static/chunks/2693-b5dbccaf1ce00a0b.js",revision:"b5dbccaf1ce00a0b"},{url:"/_next/static/chunks/3039-0e9bf08230c8ee7b.js",revision:"0e9bf08230c8ee7b"},{url:"/_next/static/chunks/3505-dc772ce29ac0b276.js",revision:"dc772ce29ac0b276"},{url:"/_next/static/chunks/3762-9e7418e6773035f2.js",revision:"9e7418e6773035f2"},{url:"/_next/static/chunks/3782-ec9e7e72c6eacec1.js",revision:"ec9e7e72c6eacec1"},{url:"/_next/static/chunks/3823-7d22f3a064856b06.js",revision:"7d22f3a064856b06"},{url:"/_next/static/chunks/4546-3be482382b443121.js",revision:"3be482382b443121"},{url:"/_next/static/chunks/4bd1b696-100b9d70ed4e49c1.js",revision:"100b9d70ed4e49c1"},{url:"/_next/static/chunks/5125-c990fc036d2a6ce4.js",revision:"c990fc036d2a6ce4"},{url:"/_next/static/chunks/5380-9004e1ac3565daca.js",revision:"9004e1ac3565daca"},{url:"/_next/static/chunks/5385-7ecda8e4ba984edc.js",revision:"7ecda8e4ba984edc"},{url:"/_next/static/chunks/5482-7535aa0aab02d518.js",revision:"7535aa0aab02d518"},{url:"/_next/static/chunks/5491-75a34ac5f4b1bc71.js",revision:"75a34ac5f4b1bc71"},{url:"/_next/static/chunks/6088-c165c565edce02be.js",revision:"c165c565edce02be"},{url:"/_next/static/chunks/6107-8fb7b82c50ce5ddd.js",revision:"8fb7b82c50ce5ddd"},{url:"/_next/static/chunks/6191.e178f0fbe1b1be57.js",revision:"e178f0fbe1b1be57"},{url:"/_next/static/chunks/670-a4ca0f366ee779f5.js",revision:"a4ca0f366ee779f5"},{url:"/_next/static/chunks/6847-ce99bc721adda9c4.js",revision:"ce99bc721adda9c4"},{url:"/_next/static/chunks/6873-ff265086321345c8.js",revision:"ff265086321345c8"},{url:"/_next/static/chunks/6886-40f1779ffff00d58.js",revision:"40f1779ffff00d58"},{url:"/_next/static/chunks/6937.f8d44316fed7bc8e.js",revision:"f8d44316fed7bc8e"},{url:"/_next/static/chunks/710-7e96cbf5d461482a.js",revision:"7e96cbf5d461482a"},{url:"/_next/static/chunks/7359-1abfb9f346309354.js",revision:"1abfb9f346309354"},{url:"/_next/static/chunks/7741-0af8b5a61d8e63d3.js",revision:"0af8b5a61d8e63d3"},{url:"/_next/static/chunks/7855-72c79224370eff7b.js",revision:"72c79224370eff7b"},{url:"/_next/static/chunks/787-032067ae978e62a8.js",revision:"032067ae978e62a8"},{url:"/_next/static/chunks/8126-48064e6c5d5794c7.js",revision:"48064e6c5d5794c7"},{url:"/_next/static/chunks/8221-d51102291d5ddaf9.js",revision:"d51102291d5ddaf9"},{url:"/_next/static/chunks/8241-eaf1b9c6054e9ad8.js",revision:"eaf1b9c6054e9ad8"},{url:"/_next/static/chunks/8466-ffa71cea7998f777.js",revision:"ffa71cea7998f777"},{url:"/_next/static/chunks/8544.74f59dd908783038.js",revision:"74f59dd908783038"},{url:"/_next/static/chunks/8746-92ff3ad56eb06d6e.js",revision:"92ff3ad56eb06d6e"},{url:"/_next/static/chunks/8876-26dea77829b2c9a0.js",revision:"26dea77829b2c9a0"},{url:"/_next/static/chunks/9205-f540995b767df00b.js",revision:"f540995b767df00b"},{url:"/_next/static/chunks/9241-01664d98236f70ec.js",revision:"01664d98236f70ec"},{url:"/_next/static/chunks/9333-17f3dbe8f3dcc2d0.js",revision:"17f3dbe8f3dcc2d0"},{url:"/_next/static/chunks/9378-4fb7500ab3ba2b2b.js",revision:"4fb7500ab3ba2b2b"},{url:"/_next/static/chunks/9392-2887c5e5703ed90a.js",revision:"2887c5e5703ed90a"},{url:"/_next/static/chunks/9397-40b8ac68e22a4d87.js",revision:"40b8ac68e22a4d87"},{url:"/_next/static/chunks/9515-53e74005e71810bd.js",revision:"53e74005e71810bd"},{url:"/_next/static/chunks/9738-c70b13d86cc3ea77.js",revision:"c70b13d86cc3ea77"},{url:"/_next/static/chunks/9958.804e47ffb4b9facb.js",revision:"804e47ffb4b9facb"},{url:"/_next/static/chunks/app/(auth)/forgot-password/page-f3956296e0f418de.js",revision:"f3956296e0f418de"},{url:"/_next/static/chunks/app/(auth)/login/page-b3531365c5e6f821.js",revision:"b3531365c5e6f821"},{url:"/_next/static/chunks/app/(auth)/onboarding/page-066f1de6cbae435f.js",revision:"066f1de6cbae435f"},{url:"/_next/static/chunks/app/(auth)/register/page-3d828ba1fce4ad94.js",revision:"3d828ba1fce4ad94"},{url:"/_next/static/chunks/app/(auth)/reset-password/page-f08b56ee59c00023.js",revision:"f08b56ee59c00023"},{url:"/_next/static/chunks/app/_not-found/page-95f11f5fe94340f1.js",revision:"95f11f5fe94340f1"},{url:"/_next/static/chunks/app/activities/page-0fa66a84232fe4a6.js",revision:"0fa66a84232fe4a6"},{url:"/_next/static/chunks/app/ai-assistant/page-68138d98581888d6.js",revision:"68138d98581888d6"},{url:"/_next/static/chunks/app/analytics/page-5186d2ccf8bbb2c5.js",revision:"5186d2ccf8bbb2c5"},{url:"/_next/static/chunks/app/api/ai/chat/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/login/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/password-reset/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/register/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/health/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/tracking/feeding/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/voice/transcribe/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/children/page-e64c77228b27a0a7.js",revision:"e64c77228b27a0a7"},{url:"/_next/static/chunks/app/family/page-f4ebe7393d633cd9.js",revision:"f4ebe7393d633cd9"},{url:"/_next/static/chunks/app/history/page-5520c77d9a874cc8.js",revision:"5520c77d9a874cc8"},{url:"/_next/static/chunks/app/insights/page-496df14d1dc8f4e2.js",revision:"496df14d1dc8f4e2"},{url:"/_next/static/chunks/app/layout-3b3ea1e64911e1a1.js",revision:"3b3ea1e64911e1a1"},{url:"/_next/static/chunks/app/legal/cookies/page-eb53496343544f2e.js",revision:"eb53496343544f2e"},{url:"/_next/static/chunks/app/legal/eula/page-97415bf431bee7dc.js",revision:"97415bf431bee7dc"},{url:"/_next/static/chunks/app/legal/page-f6328e2d2b85b2a8.js",revision:"f6328e2d2b85b2a8"},{url:"/_next/static/chunks/app/legal/privacy/page-d17db303fddc4ac3.js",revision:"d17db303fddc4ac3"},{url:"/_next/static/chunks/app/legal/terms/page-b7329caf039d4c12.js",revision:"b7329caf039d4c12"},{url:"/_next/static/chunks/app/logout/page-359b0e371fd55c32.js",revision:"359b0e371fd55c32"},{url:"/_next/static/chunks/app/offline/page-28c005360c2b2736.js",revision:"28c005360c2b2736"},{url:"/_next/static/chunks/app/page-a6468150ade2e706.js",revision:"a6468150ade2e706"},{url:"/_next/static/chunks/app/settings/page-2c07f405ef25e2e5.js",revision:"2c07f405ef25e2e5"},{url:"/_next/static/chunks/app/track/activity/page-3bea0fa7c42efb39.js",revision:"3bea0fa7c42efb39"},{url:"/_next/static/chunks/app/track/diaper/page-5bef4b4bf99ea62d.js",revision:"5bef4b4bf99ea62d"},{url:"/_next/static/chunks/app/track/feeding/page-504512141cd05584.js",revision:"504512141cd05584"},{url:"/_next/static/chunks/app/track/growth/page-33757bb4ddf9d6d5.js",revision:"33757bb4ddf9d6d5"},{url:"/_next/static/chunks/app/track/medicine/page-dfb85e9e3a1d1ec0.js",revision:"dfb85e9e3a1d1ec0"},{url:"/_next/static/chunks/app/track/page-44c8b0eca338511d.js",revision:"44c8b0eca338511d"},{url:"/_next/static/chunks/app/track/sleep/page-b8f5250996887342.js",revision:"b8f5250996887342"},{url:"/_next/static/chunks/framework-bd61ec64032c2de7.js",revision:"bd61ec64032c2de7"},{url:"/_next/static/chunks/main-520e5ec2d671abe7.js",revision:"520e5ec2d671abe7"},{url:"/_next/static/chunks/main-app-02fc3649960ba6c7.js",revision:"02fc3649960ba6c7"},{url:"/_next/static/chunks/pages/_app-4b3fb5e477a0267f.js",revision:"4b3fb5e477a0267f"},{url:"/_next/static/chunks/pages/_error-c970d8b55ace1b48.js",revision:"c970d8b55ace1b48"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-d80478e9797da650.js",revision:"d80478e9797da650"},{url:"/_next/static/css/2eb0f1dfbb62d2c0.css",revision:"2eb0f1dfbb62d2c0"},{url:"/_next/static/kxO9lbg5wBB1PCyysvYGT/_buildManifest.js",revision:"eed673ddfae39d41b2286ad066c2e53c"},{url:"/_next/static/kxO9lbg5wBB1PCyysvYGT/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/apple-touch-icon.png",revision:"fa2d4d791b90148a18d49bc3bfd7a43a"},{url:"/favicon-16x16.png",revision:"db2da3355c89a6149f6d9ee35ebe6bf3"},{url:"/favicon-32x32.png",revision:"0fd88d56aa584bd0546d05ffc63ef777"},{url:"/icon-192x192.png",revision:"b8ef7f117472c4399cceffea644eb8bd"},{url:"/icons/icon-128x128.png",revision:"96cff3b189d9c1daa1edf470290a90cd"},{url:"/icons/icon-144x144.png",revision:"b627c346c431d7e306005aec5f51baff"},{url:"/icons/icon-152x152.png",revision:"012071830c13d310e51f833baed531af"},{url:"/icons/icon-192x192.png",revision:"dfb20132ddb628237eccd4b0e2ee4aaa"},{url:"/icons/icon-384x384.png",revision:"d032b25376232878a2a29b5688992a8d"},{url:"/icons/icon-512x512.png",revision:"ffda0043571d60956f4e321cba706670"},{url:"/icons/icon-72x72.png",revision:"cc89e74126e7e1109f0186774b3c0d77"},{url:"/icons/icon-96x96.png",revision:"32813cdad5b636fc09eec01c7d705936"},{url:"/manifest.json",revision:"5cbf1ecd33b05c4772688ce7d00c2c23"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"61c6b19abff40ea7acd577be818f3976"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:a,state:c})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/api\/.*$/i,new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/.*/i,new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET")}); +if(!self.define){let e,a={};const s=(s,c)=>(s=new URL(s+".js",c).href,a[s]||new Promise(a=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=a,document.head.appendChild(e)}else e=s,importScripts(s),a()}).then(()=>{let e=a[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(c,i)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(a[n])return;let t={};const d=e=>s(e,n),r={module:{uri:n},exports:t,require:d};a[n]=Promise.all(c.map(e=>r[e]||d(e))).then(e=>(i(...e),t))}}define(["./workbox-4d767a27"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"d7500f7fee253390637ac42de2fe4521"},{url:"/_next/static/a51JgIgMTai8D3he2Zczd/_buildManifest.js",revision:"003849461a7dd4bad470c5d1d5e5254c"},{url:"/_next/static/a51JgIgMTai8D3he2Zczd/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/1213-7820689c8a23df1d.js",revision:"7820689c8a23df1d"},{url:"/_next/static/chunks/1233-aa8672e107c5a9d6.js",revision:"aa8672e107c5a9d6"},{url:"/_next/static/chunks/1255-b2f7fd83e387a9e1.js",revision:"b2f7fd83e387a9e1"},{url:"/_next/static/chunks/1280-296e0a2b6e9dd9b1.js",revision:"296e0a2b6e9dd9b1"},{url:"/_next/static/chunks/1543-530e0f57f7af68aa.js",revision:"530e0f57f7af68aa"},{url:"/_next/static/chunks/1863-7231108310f72246.js",revision:"7231108310f72246"},{url:"/_next/static/chunks/2262-26293d6453fcc927.js",revision:"26293d6453fcc927"},{url:"/_next/static/chunks/2349-f488a1827d114358.js",revision:"f488a1827d114358"},{url:"/_next/static/chunks/2449-b8a41aa6a7d2d3a4.js",revision:"b8a41aa6a7d2d3a4"},{url:"/_next/static/chunks/2619-04bc32f026a0d946.js",revision:"04bc32f026a0d946"},{url:"/_next/static/chunks/3039-0e9bf08230c8ee7b.js",revision:"0e9bf08230c8ee7b"},{url:"/_next/static/chunks/3505-dc772ce29ac0b276.js",revision:"dc772ce29ac0b276"},{url:"/_next/static/chunks/3762-c2c13ecf11b3eabb.js",revision:"c2c13ecf11b3eabb"},{url:"/_next/static/chunks/3782-ec9e7e72c6eacec1.js",revision:"ec9e7e72c6eacec1"},{url:"/_next/static/chunks/3823-7d22f3a064856b06.js",revision:"7d22f3a064856b06"},{url:"/_next/static/chunks/4546-3be482382b443121.js",revision:"3be482382b443121"},{url:"/_next/static/chunks/4bd1b696-100b9d70ed4e49c1.js",revision:"100b9d70ed4e49c1"},{url:"/_next/static/chunks/5125-c990fc036d2a6ce4.js",revision:"c990fc036d2a6ce4"},{url:"/_next/static/chunks/5380-9004e1ac3565daca.js",revision:"9004e1ac3565daca"},{url:"/_next/static/chunks/5385-7ecda8e4ba984edc.js",revision:"7ecda8e4ba984edc"},{url:"/_next/static/chunks/5482-7535aa0aab02d518.js",revision:"7535aa0aab02d518"},{url:"/_next/static/chunks/5491-75a34ac5f4b1bc71.js",revision:"75a34ac5f4b1bc71"},{url:"/_next/static/chunks/6088-c165c565edce02be.js",revision:"c165c565edce02be"},{url:"/_next/static/chunks/6107-8fb7b82c50ce5ddd.js",revision:"8fb7b82c50ce5ddd"},{url:"/_next/static/chunks/6191.e178f0fbe1b1be57.js",revision:"e178f0fbe1b1be57"},{url:"/_next/static/chunks/670-a4ca0f366ee779f5.js",revision:"a4ca0f366ee779f5"},{url:"/_next/static/chunks/6847-ce99bc721adda9c4.js",revision:"ce99bc721adda9c4"},{url:"/_next/static/chunks/6873-ff265086321345c8.js",revision:"ff265086321345c8"},{url:"/_next/static/chunks/6886-40f1779ffff00d58.js",revision:"40f1779ffff00d58"},{url:"/_next/static/chunks/6937.f8d44316fed7bc8e.js",revision:"f8d44316fed7bc8e"},{url:"/_next/static/chunks/710-7e96cbf5d461482a.js",revision:"7e96cbf5d461482a"},{url:"/_next/static/chunks/7359-1abfb9f346309354.js",revision:"1abfb9f346309354"},{url:"/_next/static/chunks/7741-0af8b5a61d8e63d3.js",revision:"0af8b5a61d8e63d3"},{url:"/_next/static/chunks/7855-72c79224370eff7b.js",revision:"72c79224370eff7b"},{url:"/_next/static/chunks/787-032067ae978e62a8.js",revision:"032067ae978e62a8"},{url:"/_next/static/chunks/8126-48064e6c5d5794c7.js",revision:"48064e6c5d5794c7"},{url:"/_next/static/chunks/8221-d51102291d5ddaf9.js",revision:"d51102291d5ddaf9"},{url:"/_next/static/chunks/8241-eaf1b9c6054e9ad8.js",revision:"eaf1b9c6054e9ad8"},{url:"/_next/static/chunks/8466-ffa71cea7998f777.js",revision:"ffa71cea7998f777"},{url:"/_next/static/chunks/8544.74f59dd908783038.js",revision:"74f59dd908783038"},{url:"/_next/static/chunks/8746-92ff3ad56eb06d6e.js",revision:"92ff3ad56eb06d6e"},{url:"/_next/static/chunks/8876-26dea77829b2c9a0.js",revision:"26dea77829b2c9a0"},{url:"/_next/static/chunks/9205-f540995b767df00b.js",revision:"f540995b767df00b"},{url:"/_next/static/chunks/9241-01664d98236f70ec.js",revision:"01664d98236f70ec"},{url:"/_next/static/chunks/9333-17f3dbe8f3dcc2d0.js",revision:"17f3dbe8f3dcc2d0"},{url:"/_next/static/chunks/9378-4fb7500ab3ba2b2b.js",revision:"4fb7500ab3ba2b2b"},{url:"/_next/static/chunks/9392-2887c5e5703ed90a.js",revision:"2887c5e5703ed90a"},{url:"/_next/static/chunks/9397-40b8ac68e22a4d87.js",revision:"40b8ac68e22a4d87"},{url:"/_next/static/chunks/9515-53e74005e71810bd.js",revision:"53e74005e71810bd"},{url:"/_next/static/chunks/9522-fc1414d221355408.js",revision:"fc1414d221355408"},{url:"/_next/static/chunks/9738-c70b13d86cc3ea77.js",revision:"c70b13d86cc3ea77"},{url:"/_next/static/chunks/9958.57780b11643f5bd9.js",revision:"57780b11643f5bd9"},{url:"/_next/static/chunks/app/(auth)/forgot-password/page-fc9fe10cd0da6b8c.js",revision:"fc9fe10cd0da6b8c"},{url:"/_next/static/chunks/app/(auth)/login/page-d9a5d5755d9f403f.js",revision:"d9a5d5755d9f403f"},{url:"/_next/static/chunks/app/(auth)/onboarding/page-2f1efbb88e25690c.js",revision:"2f1efbb88e25690c"},{url:"/_next/static/chunks/app/(auth)/register/page-562dc7bf8851de3f.js",revision:"562dc7bf8851de3f"},{url:"/_next/static/chunks/app/(auth)/reset-password/page-877e00efc3824f99.js",revision:"877e00efc3824f99"},{url:"/_next/static/chunks/app/_not-found/page-95f11f5fe94340f1.js",revision:"95f11f5fe94340f1"},{url:"/_next/static/chunks/app/ai-assistant/page-48a8ac4a67cba5b6.js",revision:"48a8ac4a67cba5b6"},{url:"/_next/static/chunks/app/analytics/page-09432cf896153f96.js",revision:"09432cf896153f96"},{url:"/_next/static/chunks/app/api/ai/chat/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/login/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/password-reset/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/register/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/health/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/tracking/feeding/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/voice/transcribe/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/children/page-8a92f3d5521369bb.js",revision:"8a92f3d5521369bb"},{url:"/_next/static/chunks/app/family/page-3bf65f3ee77dd58f.js",revision:"3bf65f3ee77dd58f"},{url:"/_next/static/chunks/app/history/page-9f7b12a4bfa73be8.js",revision:"9f7b12a4bfa73be8"},{url:"/_next/static/chunks/app/insights/page-84991e82bea095c8.js",revision:"84991e82bea095c8"},{url:"/_next/static/chunks/app/layout-9521b2b894149e0d.js",revision:"9521b2b894149e0d"},{url:"/_next/static/chunks/app/legal/cookies/page-c39a3fa6e27a8806.js",revision:"c39a3fa6e27a8806"},{url:"/_next/static/chunks/app/legal/eula/page-8015f749ab4dd660.js",revision:"8015f749ab4dd660"},{url:"/_next/static/chunks/app/legal/page-3de074f0b9741bc6.js",revision:"3de074f0b9741bc6"},{url:"/_next/static/chunks/app/legal/privacy/page-3cb58024b6fd8e21.js",revision:"3cb58024b6fd8e21"},{url:"/_next/static/chunks/app/legal/terms/page-b5a1c96cae251767.js",revision:"b5a1c96cae251767"},{url:"/_next/static/chunks/app/logout/page-359b0e371fd55c32.js",revision:"359b0e371fd55c32"},{url:"/_next/static/chunks/app/offline/page-28c005360c2b2736.js",revision:"28c005360c2b2736"},{url:"/_next/static/chunks/app/page-13b0bb5ffcd6597a.js",revision:"13b0bb5ffcd6597a"},{url:"/_next/static/chunks/app/settings/page-bacfdd22168d67f0.js",revision:"bacfdd22168d67f0"},{url:"/_next/static/chunks/app/track/activity/page-8d314ae80ae707da.js",revision:"8d314ae80ae707da"},{url:"/_next/static/chunks/app/track/diaper/page-87f3340684ba5beb.js",revision:"87f3340684ba5beb"},{url:"/_next/static/chunks/app/track/feeding/page-67ce4d6c1d0280f7.js",revision:"67ce4d6c1d0280f7"},{url:"/_next/static/chunks/app/track/growth/page-dd3130f009f9751a.js",revision:"dd3130f009f9751a"},{url:"/_next/static/chunks/app/track/medicine/page-9faf46b4c6ddcd92.js",revision:"9faf46b4c6ddcd92"},{url:"/_next/static/chunks/app/track/page-dd5ade1eb19ad389.js",revision:"dd5ade1eb19ad389"},{url:"/_next/static/chunks/app/track/sleep/page-2114dfbe2789d189.js",revision:"2114dfbe2789d189"},{url:"/_next/static/chunks/framework-bd61ec64032c2de7.js",revision:"bd61ec64032c2de7"},{url:"/_next/static/chunks/main-520e5ec2d671abe7.js",revision:"520e5ec2d671abe7"},{url:"/_next/static/chunks/main-app-02fc3649960ba6c7.js",revision:"02fc3649960ba6c7"},{url:"/_next/static/chunks/pages/_app-4b3fb5e477a0267f.js",revision:"4b3fb5e477a0267f"},{url:"/_next/static/chunks/pages/_error-c970d8b55ace1b48.js",revision:"c970d8b55ace1b48"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-b37dfa0737ad97e5.js",revision:"b37dfa0737ad97e5"},{url:"/_next/static/css/2eb0f1dfbb62d2c0.css",revision:"2eb0f1dfbb62d2c0"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/apple-touch-icon.png",revision:"fa2d4d791b90148a18d49bc3bfd7a43a"},{url:"/favicon-16x16.png",revision:"db2da3355c89a6149f6d9ee35ebe6bf3"},{url:"/favicon-32x32.png",revision:"0fd88d56aa584bd0546d05ffc63ef777"},{url:"/icon-192x192.png",revision:"b8ef7f117472c4399cceffea644eb8bd"},{url:"/icons/icon-128x128.png",revision:"96cff3b189d9c1daa1edf470290a90cd"},{url:"/icons/icon-144x144.png",revision:"b627c346c431d7e306005aec5f51baff"},{url:"/icons/icon-152x152.png",revision:"012071830c13d310e51f833baed531af"},{url:"/icons/icon-192x192.png",revision:"dfb20132ddb628237eccd4b0e2ee4aaa"},{url:"/icons/icon-384x384.png",revision:"d032b25376232878a2a29b5688992a8d"},{url:"/icons/icon-512x512.png",revision:"ffda0043571d60956f4e321cba706670"},{url:"/icons/icon-72x72.png",revision:"cc89e74126e7e1109f0186774b3c0d77"},{url:"/icons/icon-96x96.png",revision:"32813cdad5b636fc09eec01c7d705936"},{url:"/manifest.json",revision:"5cbf1ecd33b05c4772688ce7d00c2c23"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"61c6b19abff40ea7acd577be818f3976"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:a,event:s,state:c})=>a&&"opaqueredirect"===a.type?new Response(a.body,{status:200,statusText:"OK",headers:a.headers}):a}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/api\/.*$/i,new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/.*/i,new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET")});