mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
fix(tours): count a multi-day tour in the activity sums only once all its stages are on days
The activity sums never read which stage a day holds, so a multi-day tour counted its whole route on the last dated stage day while other stages were on no day yet, and a recording on one stage took the place of the whole plan beside it. With stages on days of their own, the route now counts once every stage is on a dated day of the user's, on the last of them, and is uncounted until then; a recording counts as what was done and the plan is not added beside it.
This commit is contained in:
@@ -52,6 +52,8 @@ export interface TourStatsExecutionRow {
|
||||
trip_title: string;
|
||||
assignment_id: number | null;
|
||||
date: string | null;
|
||||
/** MDT-02: the stage of a multi-day tour the day holds, by the key of its first stop; null for the whole tour. */
|
||||
tour_stage_key: string | null;
|
||||
participant_count: number;
|
||||
joined: number;
|
||||
}
|
||||
@@ -192,6 +194,7 @@ export class TourActivitiesRepository extends TrekRepository<TourActivities> {
|
||||
'tr.title as trip_title',
|
||||
'da.id as assignment_id',
|
||||
'd.date as date',
|
||||
'da.tour_stage_key as tour_stage_key',
|
||||
eb
|
||||
.selectFrom('assignment_participants as ap')
|
||||
.select(eb.fn.countAll<number>().as('n'))
|
||||
|
||||
@@ -28,6 +28,12 @@ interface ActivityStatsInput {
|
||||
tracks: readonly TourStatsTrackRow[];
|
||||
/** The tours the user left out of their own sums. */
|
||||
excluded: ReadonlySet<number>;
|
||||
/**
|
||||
* MDT-02: the stages of each multi-day tour whose stages are put on days,
|
||||
* by the key of the stop each starts at (`tourStageKeysOf`). A tour not in
|
||||
* it is counted whole, as a day tour is.
|
||||
*/
|
||||
stageKeys?: ReadonlyMap<number, readonly string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,7 +163,48 @@ function entriesOfTour(
|
||||
/** What became of one tour: its entries, or why a tour of the user's cannot count. */
|
||||
type TourOutcome = TourActivityStatsEntry[] | 'unassigned' | 'undated' | null;
|
||||
|
||||
/** The dates on which the user does one stage: its own days, and the days that hold the whole tour. */
|
||||
function stageDates(tour: TourOfStats, key: string): { any: boolean; dates: string[] } {
|
||||
const own = tour.executions.filter(
|
||||
(row) => isOwnExecution(row) && (row.tour_stage_key === null || row.tour_stage_key === key),
|
||||
);
|
||||
const dates = own.flatMap((row) => {
|
||||
const date = calendarDay(row.date);
|
||||
return date === null ? [] : [date];
|
||||
});
|
||||
return { any: own.length > 0, dates };
|
||||
}
|
||||
|
||||
/**
|
||||
* MDT-02: a multi-day tour whose stages are on days of their own. The route
|
||||
* is measured whole, so it counts once only when every stage is on a dated
|
||||
* day of the user's, on the last of those days; whether that day has passed
|
||||
* is the summary's to say. Until then it is uncounted: on no day while a
|
||||
* stage is on none of the user's days, undated while one is only on days
|
||||
* without a date. A recording counts as what was done, in place of the plan:
|
||||
* the plan of the stages it does not cover is not measured apart, so it is
|
||||
* left out rather than counted with the whole route a second time.
|
||||
*/
|
||||
function stagedOutcome(tour: TourOfStats, keys: readonly string[], input: ActivityStatsInput): TourOutcome {
|
||||
const { own, dated } = ownExecutions(tour);
|
||||
if (own === 0) return null;
|
||||
const last = dated.at(-1);
|
||||
if (last) {
|
||||
const tracks = input.tracks.filter((track) => track.place_id === tour.row.place_id);
|
||||
const recorded = entriesOfTour(tour, dated, last, tracks, input.excluded);
|
||||
if (tracks.length > 0 && recorded.every((entry) => entry.basis === 'track')) return recorded;
|
||||
}
|
||||
const stages = keys.map((key) => stageDates(tour, key));
|
||||
if (stages.some((stage) => !stage.any)) return 'unassigned';
|
||||
if (stages.some((stage) => stage.dates.length === 0) || !last) return 'undated';
|
||||
const lastDate = stages.flatMap((stage) => stage.dates).reduce((a, b) => (b > a ? b : a));
|
||||
return [planEntry(tour, { assignmentId: last.assignmentId, date: lastDate }, dated.length, input.excluded)];
|
||||
}
|
||||
|
||||
function outcomeOf(tour: TourOfStats, input: ActivityStatsInput): TourOutcome {
|
||||
const keys = input.stageKeys?.get(tour.row.place_id) ?? [];
|
||||
const stagesOnDays = keys.length > 1 && tour.executions.some((row) => row.tour_stage_key !== null);
|
||||
if (stagesOnDays) return stagedOutcome(tour, keys, input);
|
||||
const { own, dated } = ownExecutions(tour);
|
||||
const last = dated.at(-1);
|
||||
if (last) return entriesOfTour(tour, dated, last, input.tracks, input.excluded);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { TourActivities } from '../../db/entities/TourActivities.entity';
|
||||
import { TourActivityExclusions } from '../../db/entities/TourActivityExclusions.entity';
|
||||
import { TourWaypoints } from '../../db/entities/TourWaypoints.entity';
|
||||
import { Tours } from '../../db/entities/Tours.entity';
|
||||
import type { TourActivitiesRepository } from '../../db/repositories/TourActivities.repository';
|
||||
import type { TourActivitiesRepository, TourStatsExecutionRow } from '../../db/repositories/TourActivities.repository';
|
||||
import type { TourActivityExclusionsRepository } from '../../db/repositories/TourActivityExclusions.repository';
|
||||
import type { TourWaypointsRepository } from '../../db/repositories/TourWaypoints.repository';
|
||||
import type { ToursRepository } from '../../db/repositories/Tours.repository';
|
||||
import { TripMembershipService } from '../trip-membership/trip-membership.service';
|
||||
import { buildActivityStats } from './tour-activity-stats.helpers';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import type { TourActivityExclusionRequest, TourActivityStatsResponse } from '@trek/shared';
|
||||
import { tourStageKeysOf, type TourActivityExclusionRequest, type TourActivityStatsResponse } from '@trek/shared';
|
||||
|
||||
/**
|
||||
* STA-02: the user's own tour activities for the Atlas, across every trip the
|
||||
@@ -25,8 +27,21 @@ export class TourActivityStatsService {
|
||||
@InjectRepository(Tours) private readonly toursRepo: ToursRepository,
|
||||
@InjectRepository(TourActivities) private readonly activitiesRepo: TourActivitiesRepository,
|
||||
@InjectRepository(TourActivityExclusions) private readonly exclusionsRepo: TourActivityExclusionsRepository,
|
||||
@InjectRepository(TourWaypoints) private readonly waypointsRepo: TourWaypointsRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* MDT-02: the stages of each tour that has a stage on a day of its own, by
|
||||
* the key of the stop each starts at, read from the tour's saved points.
|
||||
*/
|
||||
private async stageKeysOf(executions: readonly TourStatsExecutionRow[]): Promise<Map<number, string[]>> {
|
||||
const placeIds = [...new Set(executions.filter((row) => row.tour_stage_key !== null).map((row) => row.place_id))];
|
||||
const points = await this.waypointsRepo.listForPlaces(placeIds);
|
||||
const byTour = new Map<number, typeof points>();
|
||||
for (const point of points) byTour.set(point.place_id, [...(byTour.get(point.place_id) ?? []), point]);
|
||||
return new Map([...byTour].map(([placeId, tourPoints]) => [placeId, tourStageKeysOf(tourPoints)]));
|
||||
}
|
||||
|
||||
async stats(userId: number): Promise<TourActivityStatsResponse> {
|
||||
const tripIds = await this.membership.listAccessibleTripIds(userId);
|
||||
const [executions, tracks, excluded] = await Promise.all([
|
||||
@@ -34,7 +49,8 @@ export class TourActivityStatsService {
|
||||
this.activitiesRepo.listTracksForStats(tripIds),
|
||||
this.exclusionsRepo.listPlaceIds(userId),
|
||||
]);
|
||||
return buildActivityStats({ executions, tracks, excluded: new Set(excluded) });
|
||||
const stageKeys = await this.stageKeysOf(executions);
|
||||
return buildActivityStats({ executions, tracks, excluded: new Set(excluded), stageKeys });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user