details
This commit is contained in:
@@ -38,6 +38,24 @@ export class AnalyticsController {
|
||||
return this.analyticsService.getRunningActivities(Number(weeks ?? 12));
|
||||
}
|
||||
|
||||
@Get('activities')
|
||||
activities(
|
||||
@Query('weeks') weeks?: string,
|
||||
@Query('sportType') sportType?: string,
|
||||
@Query('limit') limit?: string,
|
||||
): Promise<AnalyticsRecentActivity[]> {
|
||||
return this.analyticsService.getActivities(
|
||||
Number(weeks ?? 52),
|
||||
sportType,
|
||||
Number(limit ?? 100),
|
||||
);
|
||||
}
|
||||
|
||||
@Get('activities/:id')
|
||||
activity(@Param('id') id: string): Promise<Record<string, unknown>> {
|
||||
return this.analyticsService.getActivityDetail(id);
|
||||
}
|
||||
|
||||
@Get('running/activities/:id')
|
||||
runningActivity(@Param('id') id: string): Promise<RunningActivityDetail> {
|
||||
return this.analyticsService.getRunningActivityDetail(id);
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import {
|
||||
StravaActivityEntity,
|
||||
StravaActivityStreamPointEntity,
|
||||
StravaStrengthTrainingDetailEntity,
|
||||
StravaSwimActivityDetailEntity,
|
||||
} from '../database/entities';
|
||||
import { StravaModule } from '../strava/strava.module';
|
||||
import { AnalyticsController } from './analytics.controller';
|
||||
@@ -14,6 +16,8 @@ import { AnalyticsService } from './analytics.service';
|
||||
TypeOrmModule.forFeature([
|
||||
StravaActivityEntity,
|
||||
StravaActivityStreamPointEntity,
|
||||
StravaStrengthTrainingDetailEntity,
|
||||
StravaSwimActivityDetailEntity,
|
||||
]),
|
||||
],
|
||||
controllers: [AnalyticsController],
|
||||
|
||||
@@ -31,7 +31,14 @@ describe('AnalyticsService', () => {
|
||||
.mockResolvedValue(streamPoints.length);
|
||||
const streamPointRepository = {
|
||||
find: streamPointFind,
|
||||
count: jest.fn().mockResolvedValue(streamPoints.length),
|
||||
} as unknown as Repository<StravaActivityStreamPointEntity>;
|
||||
const strengthTrainingDetailRepository = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
const swimActivityDetailRepository = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
const stravaStreamImportService = {
|
||||
importStreamsForActivity,
|
||||
} as unknown as StravaStreamImportService;
|
||||
@@ -40,6 +47,8 @@ describe('AnalyticsService', () => {
|
||||
service: new AnalyticsService(
|
||||
activityRepository,
|
||||
streamPointRepository,
|
||||
strengthTrainingDetailRepository as never,
|
||||
swimActivityDetailRepository as never,
|
||||
stravaStreamImportService,
|
||||
),
|
||||
activityRepository,
|
||||
|
||||
@@ -4,6 +4,8 @@ import { In, MoreThanOrEqual, Repository } from 'typeorm';
|
||||
import {
|
||||
StravaActivityEntity,
|
||||
StravaActivityStreamPointEntity,
|
||||
StravaStrengthTrainingDetailEntity,
|
||||
StravaSwimActivityDetailEntity,
|
||||
} from '../database/entities';
|
||||
import { StravaStreamImportService } from '../strava/strava-stream-import.service';
|
||||
import {
|
||||
@@ -46,6 +48,10 @@ export class AnalyticsService {
|
||||
private readonly activityRepository: Repository<StravaActivityEntity>,
|
||||
@InjectRepository(StravaActivityStreamPointEntity)
|
||||
private readonly streamPointRepository: Repository<StravaActivityStreamPointEntity>,
|
||||
@InjectRepository(StravaStrengthTrainingDetailEntity)
|
||||
private readonly strengthTrainingDetailRepository: Repository<StravaStrengthTrainingDetailEntity>,
|
||||
@InjectRepository(StravaSwimActivityDetailEntity)
|
||||
private readonly swimActivityDetailRepository: Repository<StravaSwimActivityDetailEntity>,
|
||||
private readonly stravaStreamImportService: StravaStreamImportService,
|
||||
) {}
|
||||
|
||||
@@ -262,6 +268,80 @@ export class AnalyticsService {
|
||||
.map((activity) => this.toRecentActivity(activity));
|
||||
}
|
||||
|
||||
async getActivities(
|
||||
weeksInput = 52,
|
||||
sportTypeInput?: string,
|
||||
limitInput = 100,
|
||||
): Promise<AnalyticsRecentActivity[]> {
|
||||
const weeks = this.clampWeeks(weeksInput);
|
||||
const limit = this.clamp(limitInput, 1, 200);
|
||||
const selectedSportType = this.normalizeSportType(sportTypeInput);
|
||||
const rangeStart = this.startOfWeek(
|
||||
this.addDays(new Date(), -(weeks - 1) * 7),
|
||||
);
|
||||
const activities = await this.findActivitiesInRange(rangeStart);
|
||||
|
||||
return activities
|
||||
.filter((activity) =>
|
||||
selectedSportType
|
||||
? (activity.sportType ?? 'Unbekannt') === selectedSportType
|
||||
: true,
|
||||
)
|
||||
.slice(0, limit)
|
||||
.map((activity) => this.toRecentActivity(activity));
|
||||
}
|
||||
|
||||
async getActivityDetail(id: string): Promise<Record<string, unknown>> {
|
||||
const activity = await this.activityRepository.findOne({
|
||||
where: [{ id }, { stravaActivityId: id }],
|
||||
});
|
||||
if (!activity) {
|
||||
throw new NotFoundException('Activity not found');
|
||||
}
|
||||
|
||||
const [streamPointCount, strengthTrainingDetail, swimActivityDetail] =
|
||||
await Promise.all([
|
||||
this.streamPointRepository.count({ where: { activityId: activity.id } }),
|
||||
this.strengthTrainingDetailRepository.findOne({
|
||||
where: { activityId: activity.id },
|
||||
}),
|
||||
this.swimActivityDetailRepository.findOne({
|
||||
where: { activityId: activity.id },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
...this.toRecentActivity(activity),
|
||||
startDateLocal: activity.startDateLocal
|
||||
? activity.startDateLocal.toISOString()
|
||||
: null,
|
||||
elapsedTimeSeconds: activity.elapsedTime,
|
||||
totalElevationGainMeters: activity.totalElevationGain,
|
||||
maxSpeedMetersPerSecond: activity.maxSpeed,
|
||||
maxHeartrate: activity.maxHeartrate,
|
||||
averageWatts: activity.averageWatts,
|
||||
maxWatts: activity.maxWatts,
|
||||
weightedAverageWatts: activity.weightedAverageWatts,
|
||||
averageCadence: activity.averageCadence,
|
||||
calories: activity.calories,
|
||||
gearId: activity.gearId,
|
||||
trainer: activity.trainer,
|
||||
commute: activity.commute,
|
||||
manual: activity.manual,
|
||||
private: activity.private,
|
||||
visibility: activity.visibility,
|
||||
mapId: activity.mapId,
|
||||
summaryPolyline: activity.summaryPolyline,
|
||||
streamPointCount,
|
||||
strengthTrainingDetail: strengthTrainingDetail
|
||||
? this.toStrengthTrainingDetail(strengthTrainingDetail)
|
||||
: null,
|
||||
swimActivityDetail: swimActivityDetail
|
||||
? this.toSwimActivityDetail(swimActivityDetail)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
async getRunningActivityDetail(id: string): Promise<RunningActivityDetail> {
|
||||
const activity = await this.activityRepository.findOne({ where: { id } });
|
||||
if (!activity || !this.isRunningActivity(activity)) {
|
||||
@@ -982,6 +1062,46 @@ export class AnalyticsService {
|
||||
};
|
||||
}
|
||||
|
||||
private toStrengthTrainingDetail(
|
||||
detail: StravaStrengthTrainingDetailEntity,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
movingTimeSeconds: detail.movingTime,
|
||||
elapsedTimeSeconds: detail.elapsedTime,
|
||||
calories: detail.calories,
|
||||
averageHeartrate: detail.averageHeartrate,
|
||||
maxHeartrate: detail.maxHeartrate,
|
||||
perceivedExertion: detail.perceivedExertion,
|
||||
workoutType: detail.workoutType,
|
||||
description: detail.description,
|
||||
rawPayload: detail.rawPayload,
|
||||
};
|
||||
}
|
||||
|
||||
private toSwimActivityDetail(
|
||||
detail: StravaSwimActivityDetailEntity,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
distanceMeters: detail.distance,
|
||||
movingTimeSeconds: detail.movingTime,
|
||||
elapsedTimeSeconds: detail.elapsedTime,
|
||||
averageSpeedMetersPerSecond: detail.averageSpeed,
|
||||
maxSpeedMetersPerSecond: detail.maxSpeed,
|
||||
paceSecondsPer100m:
|
||||
detail.distance && detail.distance > 0 && detail.movingTime
|
||||
? Math.round(detail.movingTime / (detail.distance / 100))
|
||||
: null,
|
||||
calories: detail.calories,
|
||||
averageHeartrate: detail.averageHeartrate,
|
||||
maxHeartrate: detail.maxHeartrate,
|
||||
averageCadence: detail.averageCadence,
|
||||
poolLength: detail.poolLength,
|
||||
laps: detail.laps,
|
||||
lapsError: detail.lapsError,
|
||||
rawPayload: detail.rawPayload,
|
||||
};
|
||||
}
|
||||
|
||||
private createTotals(): AnalyticsTotals {
|
||||
return {
|
||||
activityCount: 0,
|
||||
|
||||
Reference in New Issue
Block a user