feat: Implement analytics comparison endpoint for multi-child analysis
Comparison API:
- GET /api/v1/analytics/compare?childIds=id1,id2&metric=sleep-patterns&startDate=...&endDate=...
- Supports 2+ children comparison
- Validates user access to all children
- Date range validation with ISO 8601 format
Supported Metrics:
- sleep-patterns: Average hours, night sleep %, nap frequency, daily breakdown
- feeding-frequency: Total feedings, average per day, methods, amounts
- diaper-changes: Total changes, wet/dirty/both breakdown
- activities: Total count, types distribution, most common activity
ComparisonService:
- compareSleepPatterns() - Detailed sleep analysis with night vs day sleep
- compareFeedingFrequency() - Feeding patterns and methods tracking
- compareDiaperChanges() - Diaper change patterns by type
- compareActivities() - Activity type distribution and frequency
Response Structure:
{
metric: "sleep-patterns",
dateRange: { start, end },
children: [{
childId, childName, age,
data: { detailed metrics },
summary: { key stats }
}],
overallSummary: {
averageAcrossChildren,
childWithMost/Least,
...
}
}
Daily Breakdown:
- Each metric includes day-by-day data
- Allows chart rendering on frontend
- Supports trend analysis
Use Cases:
- "How does Emma's sleep compare to Noah's?"
- "Which child eats more frequently?"
- "Compare diaper patterns for twins"
- Side-by-side analytics dashboards
Security:
- Family-level permission checks
- User must have access to all children
- Cross-family comparison supported
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,14 @@ import {
|
||||
Req,
|
||||
Res,
|
||||
Header,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { PatternAnalysisService } from './pattern-analysis.service';
|
||||
import { PredictionService } from './prediction.service';
|
||||
import { ReportService } from './report.service';
|
||||
import { ComparisonService } from './comparison.service';
|
||||
import { ComparisonMetric } from './dto/comparison.dto';
|
||||
|
||||
@Controller('api/v1/analytics')
|
||||
export class AnalyticsController {
|
||||
@@ -18,6 +21,7 @@ export class AnalyticsController {
|
||||
private readonly patternAnalysisService: PatternAnalysisService,
|
||||
private readonly predictionService: PredictionService,
|
||||
private readonly reportService: ReportService,
|
||||
private readonly comparisonService: ComparisonService,
|
||||
) {}
|
||||
|
||||
@Get('insights/:childId')
|
||||
@@ -128,4 +132,69 @@ export class AnalyticsController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Get('compare')
|
||||
async compareChildren(
|
||||
@Req() req: any,
|
||||
@Query('childIds') childIdsParam: string,
|
||||
@Query('metric') metricParam: string,
|
||||
@Query('startDate') startDate: string,
|
||||
@Query('endDate') endDate: string,
|
||||
) {
|
||||
// Validate required parameters
|
||||
if (!childIdsParam) {
|
||||
throw new BadRequestException('childIds query parameter is required (comma-separated)');
|
||||
}
|
||||
|
||||
if (!metricParam) {
|
||||
throw new BadRequestException('metric query parameter is required');
|
||||
}
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
throw new BadRequestException('startDate and endDate query parameters are required');
|
||||
}
|
||||
|
||||
// Parse child IDs
|
||||
const childIds = childIdsParam.split(',').filter(id => id.trim());
|
||||
|
||||
if (childIds.length < 2) {
|
||||
throw new BadRequestException('At least 2 children are required for comparison');
|
||||
}
|
||||
|
||||
// Validate metric
|
||||
const validMetrics = Object.values(ComparisonMetric);
|
||||
if (!validMetrics.includes(metricParam as ComparisonMetric)) {
|
||||
throw new BadRequestException(
|
||||
`Invalid metric. Must be one of: ${validMetrics.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const metric = metricParam as ComparisonMetric;
|
||||
|
||||
// Parse dates
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||
throw new BadRequestException('Invalid date format. Use ISO 8601 format (YYYY-MM-DD)');
|
||||
}
|
||||
|
||||
if (start > end) {
|
||||
throw new BadRequestException('startDate must be before endDate');
|
||||
}
|
||||
|
||||
// Perform comparison
|
||||
const comparison = await this.comparisonService.compareChildren(
|
||||
req.user.userId,
|
||||
childIds,
|
||||
metric,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: comparison,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Activity } from '../../database/entities/activity.entity';
|
||||
import { Child } from '../../database/entities/child.entity';
|
||||
import { FamilyMember } from '../../database/entities/family-member.entity';
|
||||
import { PatternAnalysisService } from './pattern-analysis.service';
|
||||
import { PredictionService } from './prediction.service';
|
||||
import { ReportService } from './report.service';
|
||||
import { ComparisonService } from './comparison.service';
|
||||
import { AnalyticsController } from './analytics.controller';
|
||||
import { InsightsController } from './insights.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Activity, Child])],
|
||||
imports: [TypeOrmModule.forFeature([Activity, Child, FamilyMember])],
|
||||
controllers: [AnalyticsController, InsightsController],
|
||||
providers: [PatternAnalysisService, PredictionService, ReportService],
|
||||
exports: [PatternAnalysisService, PredictionService, ReportService],
|
||||
providers: [PatternAnalysisService, PredictionService, ReportService, ComparisonService],
|
||||
exports: [PatternAnalysisService, PredictionService, ReportService, ComparisonService],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
import { Injectable, ForbiddenException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Between } from 'typeorm';
|
||||
import { Activity, ActivityType } from '../../database/entities/activity.entity';
|
||||
import { Child } from '../../database/entities/child.entity';
|
||||
import { FamilyMember } from '../../database/entities/family-member.entity';
|
||||
import {
|
||||
ComparisonMetric,
|
||||
ComparisonResult,
|
||||
ComparisonChildData,
|
||||
SleepComparisonData,
|
||||
FeedingComparisonData,
|
||||
DiapeChangesComparisonData,
|
||||
ActivitiesComparisonData,
|
||||
} from './dto/comparison.dto';
|
||||
import {
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
eachDayOfInterval,
|
||||
differenceInMonths,
|
||||
format,
|
||||
isWithinInterval,
|
||||
parseISO,
|
||||
} from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class ComparisonService {
|
||||
constructor(
|
||||
@InjectRepository(Activity)
|
||||
private activityRepository: Repository<Activity>,
|
||||
@InjectRepository(Child)
|
||||
private childRepository: Repository<Child>,
|
||||
@InjectRepository(FamilyMember)
|
||||
private familyMemberRepository: Repository<FamilyMember>,
|
||||
) {}
|
||||
|
||||
async compareChildren(
|
||||
userId: string,
|
||||
childIds: string[],
|
||||
metric: ComparisonMetric,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): Promise<ComparisonResult> {
|
||||
// Validate at least 2 children
|
||||
if (childIds.length < 2) {
|
||||
throw new ForbiddenException('At least 2 children required for comparison');
|
||||
}
|
||||
|
||||
// Verify all children exist and user has access
|
||||
const children = await this.childRepository
|
||||
.createQueryBuilder('child')
|
||||
.where('child.id IN (:...childIds)', { childIds })
|
||||
.andWhere('child.deletedAt IS NULL')
|
||||
.getMany();
|
||||
|
||||
if (children.length !== childIds.length) {
|
||||
throw new ForbiddenException('One or more children not found');
|
||||
}
|
||||
|
||||
// Verify user has access to all children
|
||||
const familyIds = [...new Set(children.map((c) => c.familyId))];
|
||||
for (const familyId of familyIds) {
|
||||
const membership = await this.familyMemberRepository.findOne({
|
||||
where: { userId, familyId },
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have access to one or more children',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Route to appropriate comparison method
|
||||
switch (metric) {
|
||||
case ComparisonMetric.SLEEP_PATTERNS:
|
||||
return this.compareSleepPatterns(children, startDate, endDate);
|
||||
case ComparisonMetric.FEEDING_FREQUENCY:
|
||||
return this.compareFeedingFrequency(children, startDate, endDate);
|
||||
case ComparisonMetric.DIAPER_CHANGES:
|
||||
return this.compareDiaperChanges(children, startDate, endDate);
|
||||
case ComparisonMetric.ACTIVITIES:
|
||||
return this.compareActivities(children, startDate, endDate);
|
||||
default:
|
||||
throw new ForbiddenException(`Unsupported metric: ${metric}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async compareSleepPatterns(
|
||||
children: Child[],
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): Promise<ComparisonResult> {
|
||||
const childrenData: ComparisonChildData[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
const sleepActivities = await this.activityRepository.find({
|
||||
where: {
|
||||
childId: child.id,
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: Between(startOfDay(startDate), endOfDay(endDate)),
|
||||
},
|
||||
order: { startedAt: 'ASC' },
|
||||
});
|
||||
|
||||
const data = this.calculateSleepData(sleepActivities, startDate, endDate);
|
||||
const age = differenceInMonths(new Date(), new Date(child.birthDate));
|
||||
|
||||
childrenData.push({
|
||||
childId: child.id,
|
||||
childName: child.name,
|
||||
age,
|
||||
data,
|
||||
summary: {
|
||||
averageHoursPerDay: data.averageHoursPerDay,
|
||||
totalSessions: data.totalSleepSessions,
|
||||
nightSleepPercentage: data.nightSleepPercentage,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate overall summary
|
||||
const overallSummary = {
|
||||
averageHoursAcrossChildren:
|
||||
childrenData.reduce((sum, c) => sum + c.data.averageHoursPerDay, 0) /
|
||||
childrenData.length,
|
||||
childWithMostSleep: childrenData.reduce((prev, current) =>
|
||||
prev.data.averageHoursPerDay > current.data.averageHoursPerDay
|
||||
? prev
|
||||
: current,
|
||||
).childName,
|
||||
childWithLeastSleep: childrenData.reduce((prev, current) =>
|
||||
prev.data.averageHoursPerDay < current.data.averageHoursPerDay
|
||||
? prev
|
||||
: current,
|
||||
).childName,
|
||||
};
|
||||
|
||||
return {
|
||||
metric: ComparisonMetric.SLEEP_PATTERNS,
|
||||
dateRange: { start: startDate, end: endDate },
|
||||
children: childrenData,
|
||||
overallSummary,
|
||||
};
|
||||
}
|
||||
|
||||
private calculateSleepData(
|
||||
activities: Activity[],
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): SleepComparisonData {
|
||||
const days = eachDayOfInterval({ start: startDate, end: endDate });
|
||||
const totalDays = days.length;
|
||||
|
||||
let totalMinutes = 0;
|
||||
let nightSleepMinutes = 0;
|
||||
let longestSleepDuration = 0;
|
||||
|
||||
const dailyBreakdown = days.map((day) => {
|
||||
const dayActivities = activities.filter((a) =>
|
||||
isWithinInterval(new Date(a.startedAt), {
|
||||
start: startOfDay(day),
|
||||
end: endOfDay(day),
|
||||
}),
|
||||
);
|
||||
|
||||
let dayTotalMinutes = 0;
|
||||
dayActivities.forEach((activity) => {
|
||||
const start = new Date(activity.startedAt);
|
||||
const end = activity.endedAt
|
||||
? new Date(activity.endedAt)
|
||||
: new Date();
|
||||
const duration = Math.floor((end.getTime() - start.getTime()) / 60000);
|
||||
|
||||
dayTotalMinutes += duration;
|
||||
totalMinutes += duration;
|
||||
|
||||
if (duration > longestSleepDuration) {
|
||||
longestSleepDuration = duration;
|
||||
}
|
||||
|
||||
// Check if night sleep (8PM - 6AM)
|
||||
const hour = start.getHours();
|
||||
if (hour >= 20 || hour < 6) {
|
||||
nightSleepMinutes += duration;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
date: format(day, 'yyyy-MM-dd'),
|
||||
totalMinutes: dayTotalMinutes,
|
||||
sessionsCount: dayActivities.length,
|
||||
};
|
||||
});
|
||||
|
||||
// Count naps (sleep sessions during 6AM - 8PM)
|
||||
const naps = activities.filter((a) => {
|
||||
const hour = new Date(a.startedAt).getHours();
|
||||
return hour >= 6 && hour < 20;
|
||||
});
|
||||
|
||||
return {
|
||||
averageHoursPerDay: totalMinutes / totalDays / 60,
|
||||
totalSleepSessions: activities.length,
|
||||
longestSleepDuration,
|
||||
averageNapsPerDay: naps.length / totalDays,
|
||||
nightSleepPercentage:
|
||||
totalMinutes > 0 ? (nightSleepMinutes / totalMinutes) * 100 : 0,
|
||||
dailyBreakdown,
|
||||
};
|
||||
}
|
||||
|
||||
private async compareFeedingFrequency(
|
||||
children: Child[],
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): Promise<ComparisonResult> {
|
||||
const childrenData: ComparisonChildData[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
const feedingActivities = await this.activityRepository.find({
|
||||
where: {
|
||||
childId: child.id,
|
||||
type: ActivityType.FEEDING,
|
||||
startedAt: Between(startOfDay(startDate), endOfDay(endDate)),
|
||||
},
|
||||
order: { startedAt: 'ASC' },
|
||||
});
|
||||
|
||||
const data = this.calculateFeedingData(
|
||||
feedingActivities,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
const age = differenceInMonths(new Date(), new Date(child.birthDate));
|
||||
|
||||
childrenData.push({
|
||||
childId: child.id,
|
||||
childName: child.name,
|
||||
age,
|
||||
data,
|
||||
summary: {
|
||||
totalFeedings: data.totalFeedings,
|
||||
averagePerDay: data.averageFeedingsPerDay,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const overallSummary = {
|
||||
averageFeedingsAcrossChildren:
|
||||
childrenData.reduce((sum, c) => sum + c.data.averageFeedingsPerDay, 0) /
|
||||
childrenData.length,
|
||||
childWithMostFeedings: childrenData.reduce((prev, current) =>
|
||||
prev.data.totalFeedings > current.data.totalFeedings ? prev : current,
|
||||
).childName,
|
||||
};
|
||||
|
||||
return {
|
||||
metric: ComparisonMetric.FEEDING_FREQUENCY,
|
||||
dateRange: { start: startDate, end: endDate },
|
||||
children: childrenData,
|
||||
overallSummary,
|
||||
};
|
||||
}
|
||||
|
||||
private calculateFeedingData(
|
||||
activities: Activity[],
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): FeedingComparisonData {
|
||||
const days = eachDayOfInterval({ start: startDate, end: endDate });
|
||||
const totalDays = days.length;
|
||||
|
||||
const feedingMethods: Record<string, number> = {};
|
||||
let totalAmount = 0;
|
||||
let amountCount = 0;
|
||||
|
||||
const dailyBreakdown = days.map((day) => {
|
||||
const dayActivities = activities.filter((a) =>
|
||||
isWithinInterval(new Date(a.startedAt), {
|
||||
start: startOfDay(day),
|
||||
end: endOfDay(day),
|
||||
}),
|
||||
);
|
||||
|
||||
let dayTotalAmount = 0;
|
||||
dayActivities.forEach((activity) => {
|
||||
const method = activity.metadata?.method || 'unknown';
|
||||
feedingMethods[method] = (feedingMethods[method] || 0) + 1;
|
||||
|
||||
if (activity.metadata?.amount) {
|
||||
const amount = parseFloat(activity.metadata.amount);
|
||||
totalAmount += amount;
|
||||
dayTotalAmount += amount;
|
||||
amountCount++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
date: format(day, 'yyyy-MM-dd'),
|
||||
count: dayActivities.length,
|
||||
totalAmount: dayTotalAmount > 0 ? dayTotalAmount : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
totalFeedings: activities.length,
|
||||
averageFeedingsPerDay: activities.length / totalDays,
|
||||
feedingMethods,
|
||||
averageAmountPerFeeding:
|
||||
amountCount > 0 ? totalAmount / amountCount : undefined,
|
||||
dailyBreakdown,
|
||||
};
|
||||
}
|
||||
|
||||
private async compareDiaperChanges(
|
||||
children: Child[],
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): Promise<ComparisonResult> {
|
||||
const childrenData: ComparisonChildData[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
const diaperActivities = await this.activityRepository.find({
|
||||
where: {
|
||||
childId: child.id,
|
||||
type: ActivityType.DIAPER,
|
||||
startedAt: Between(startOfDay(startDate), endOfDay(endDate)),
|
||||
},
|
||||
order: { startedAt: 'ASC' },
|
||||
});
|
||||
|
||||
const data = this.calculateDiaperData(
|
||||
diaperActivities,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
const age = differenceInMonths(new Date(), new Date(child.birthDate));
|
||||
|
||||
childrenData.push({
|
||||
childId: child.id,
|
||||
childName: child.name,
|
||||
age,
|
||||
data,
|
||||
summary: {
|
||||
totalChanges: data.totalChanges,
|
||||
averagePerDay: data.averageChangesPerDay,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
metric: ComparisonMetric.DIAPER_CHANGES,
|
||||
dateRange: { start: startDate, end: endDate },
|
||||
children: childrenData,
|
||||
};
|
||||
}
|
||||
|
||||
private calculateDiaperData(
|
||||
activities: Activity[],
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): DiapeChangesComparisonData {
|
||||
const days = eachDayOfInterval({ start: startDate, end: endDate });
|
||||
const totalDays = days.length;
|
||||
|
||||
let wetDiapers = 0;
|
||||
let dirtyDiapers = 0;
|
||||
let bothDiapers = 0;
|
||||
|
||||
const dailyBreakdown = days.map((day) => {
|
||||
const dayActivities = activities.filter((a) =>
|
||||
isWithinInterval(new Date(a.startedAt), {
|
||||
start: startOfDay(day),
|
||||
end: endOfDay(day),
|
||||
}),
|
||||
);
|
||||
|
||||
let dayWet = 0;
|
||||
let dayDirty = 0;
|
||||
let dayBoth = 0;
|
||||
|
||||
dayActivities.forEach((activity) => {
|
||||
const type = activity.metadata?.type || 'wet';
|
||||
if (type === 'wet') {
|
||||
wetDiapers++;
|
||||
dayWet++;
|
||||
} else if (type === 'dirty') {
|
||||
dirtyDiapers++;
|
||||
dayDirty++;
|
||||
} else if (type === 'both') {
|
||||
bothDiapers++;
|
||||
dayBoth++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
date: format(day, 'yyyy-MM-dd'),
|
||||
wet: dayWet,
|
||||
dirty: dayDirty,
|
||||
both: dayBoth,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
totalChanges: activities.length,
|
||||
averageChangesPerDay: activities.length / totalDays,
|
||||
wetDiapers,
|
||||
dirtyDiapers,
|
||||
bothDiapers,
|
||||
dailyBreakdown,
|
||||
};
|
||||
}
|
||||
|
||||
private async compareActivities(
|
||||
children: Child[],
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): Promise<ComparisonResult> {
|
||||
const childrenData: ComparisonChildData[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
const activities = await this.activityRepository.find({
|
||||
where: {
|
||||
childId: child.id,
|
||||
type: ActivityType.ACTIVITY,
|
||||
startedAt: Between(startOfDay(startDate), endOfDay(endDate)),
|
||||
},
|
||||
order: { startedAt: 'ASC' },
|
||||
});
|
||||
|
||||
const data = this.calculateActivitiesData(activities, startDate, endDate);
|
||||
const age = differenceInMonths(new Date(), new Date(child.birthDate));
|
||||
|
||||
childrenData.push({
|
||||
childId: child.id,
|
||||
childName: child.name,
|
||||
age,
|
||||
data,
|
||||
summary: {
|
||||
totalActivities: data.totalActivities,
|
||||
averagePerDay: data.averageActivitiesPerDay,
|
||||
mostCommon: data.mostCommonActivity,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
metric: ComparisonMetric.ACTIVITIES,
|
||||
dateRange: { start: startDate, end: endDate },
|
||||
children: childrenData,
|
||||
};
|
||||
}
|
||||
|
||||
private calculateActivitiesData(
|
||||
activities: Activity[],
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): ActivitiesComparisonData {
|
||||
const days = eachDayOfInterval({ start: startDate, end: endDate });
|
||||
const totalDays = days.length;
|
||||
|
||||
const activitiesByType: Record<string, number> = {};
|
||||
|
||||
const dailyBreakdown = days.map((day) => {
|
||||
const dayActivities = activities.filter((a) =>
|
||||
isWithinInterval(new Date(a.startedAt), {
|
||||
start: startOfDay(day),
|
||||
end: endOfDay(day),
|
||||
}),
|
||||
);
|
||||
|
||||
const dayTypes: Record<string, number> = {};
|
||||
dayActivities.forEach((activity) => {
|
||||
const activityType =
|
||||
activity.metadata?.activityType || 'unspecified';
|
||||
activitiesByType[activityType] =
|
||||
(activitiesByType[activityType] || 0) + 1;
|
||||
dayTypes[activityType] = (dayTypes[activityType] || 0) + 1;
|
||||
});
|
||||
|
||||
return {
|
||||
date: format(day, 'yyyy-MM-dd'),
|
||||
count: dayActivities.length,
|
||||
types: dayTypes,
|
||||
};
|
||||
});
|
||||
|
||||
// Find most common activity
|
||||
const mostCommonActivity =
|
||||
Object.keys(activitiesByType).length > 0
|
||||
? Object.entries(activitiesByType).reduce((prev, current) =>
|
||||
prev[1] > current[1] ? prev : current,
|
||||
)[0]
|
||||
: 'none';
|
||||
|
||||
return {
|
||||
totalActivities: activities.length,
|
||||
averageActivitiesPerDay: activities.length / totalDays,
|
||||
activitiesByType,
|
||||
mostCommonActivity,
|
||||
dailyBreakdown,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
export enum ComparisonMetric {
|
||||
SLEEP_PATTERNS = 'sleep-patterns',
|
||||
FEEDING_FREQUENCY = 'feeding-frequency',
|
||||
GROWTH_CURVES = 'growth-curves',
|
||||
DIAPER_CHANGES = 'diaper-changes',
|
||||
ACTIVITIES = 'activities',
|
||||
}
|
||||
|
||||
export interface ComparisonChildData {
|
||||
childId: string;
|
||||
childName: string;
|
||||
age: number; // in months
|
||||
data: any;
|
||||
summary: any;
|
||||
}
|
||||
|
||||
export interface ComparisonResult {
|
||||
metric: ComparisonMetric;
|
||||
dateRange: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
children: ComparisonChildData[];
|
||||
overallSummary?: any;
|
||||
}
|
||||
|
||||
export interface SleepComparisonData {
|
||||
averageHoursPerDay: number;
|
||||
totalSleepSessions: number;
|
||||
longestSleepDuration: number; // in minutes
|
||||
averageNapsPerDay: number;
|
||||
nightSleepPercentage: number; // percentage of sleep during night (8PM-6AM)
|
||||
dailyBreakdown: Array<{
|
||||
date: string;
|
||||
totalMinutes: number;
|
||||
sessionsCount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface FeedingComparisonData {
|
||||
totalFeedings: number;
|
||||
averageFeedingsPerDay: number;
|
||||
feedingMethods: Record<string, number>; // { bottle: 10, breast: 5 }
|
||||
averageAmountPerFeeding?: number; // if tracked
|
||||
dailyBreakdown: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
totalAmount?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DiapeChangesComparisonData {
|
||||
totalChanges: number;
|
||||
averageChangesPerDay: number;
|
||||
wetDiapers: number;
|
||||
dirtyDiapers: number;
|
||||
bothDiapers: number;
|
||||
dailyBreakdown: Array<{
|
||||
date: string;
|
||||
wet: number;
|
||||
dirty: number;
|
||||
both: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ActivitiesComparisonData {
|
||||
totalActivities: number;
|
||||
averageActivitiesPerDay: number;
|
||||
activitiesByType: Record<string, number>;
|
||||
mostCommonActivity: string;
|
||||
dailyBreakdown: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
types: Record<string, number>;
|
||||
}>;
|
||||
}
|
||||
Reference in New Issue
Block a user