GoalMetric

This commit is contained in:
2026-05-17 14:39:21 +02:00
parent 3b710ee324
commit e32e7589a3
5 changed files with 109 additions and 49 deletions

View File

@@ -33,12 +33,15 @@ public class ActivitySummary extends AbstractActivity implements ReadonlyEntity
@Transient
private @Nullable Map<String, String> valuesForSummary;
@SuppressWarnings("NullAway.Init")
public ActivitySummary() {
// hibernate
}
@PostLoad
void populateValuesForSummary() {
final Map<String, String> map = new HashMap<>();
if (distanceM != null) {
map.put("distanceM", String.valueOf(distanceM));
}
map.put("distanceM", String.valueOf(distanceM));
if (trainingLoad != null) {
map.put("trainingLoad", String.valueOf(trainingLoad));
}
@@ -50,9 +53,4 @@ public class ActivitySummary extends AbstractActivity implements ReadonlyEntity
}
valuesForSummary = map;
}
@SuppressWarnings("NullAway.Init")
public ActivitySummary() {
// hibernate
}
}

View File

@@ -62,15 +62,14 @@ public class DashboardService {
final Map<Long, List<ActivityStreamPoint>> weekActivityStreams =
resolveWeekActivityStreams(activitySummaries, selectedWeekMonday);
final LocalDate weekMonday = selectedWeekMonday;
final int calendarYear = weekMonday.getYear();
final int calendarMonth = weekMonday.getMonthValue();
final int calendarYear = selectedWeekMonday.getYear();
final int calendarMonth = selectedWeekMonday.getMonthValue();
final List<GoalMetricTotals> metricTotals = java.util.Arrays.stream(GoalMetricEnum.values())
.map(metric -> new GoalMetricTotals(
metric,
goalMetricPeriodTotalService.getWeeklyTotal(athleteId, metric, year, calendarWeek),
goalMetricPeriodTotalService.getMonthlyTotal(athleteId, metric, calendarYear, calendarMonth),
goalMetricPeriodTotalService.getYearlyTotal(athleteId, metric, calendarYear)
goalMetricPeriodTotalService.getMonthlyTotalForWeek(athleteId, metric, year, calendarWeek, calendarYear, calendarMonth),
goalMetricPeriodTotalService.getYearlyTotalForWeek(athleteId, metric, year, calendarWeek, calendarYear)
))
.toList();

View File

@@ -63,6 +63,48 @@ public class GoalMetricPeriodTotalDAO extends AbstractDAO<GoalMetricPeriodTotal>
return result != null ? result : 0.0;
}
public double sumByWeekAndMonth(
final long athleteId,
final GoalMetricEnum metric,
final int isoWeekYear,
final int calendarWeek,
final int year,
final int month) {
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<Double> cq = cb.createQuery(Double.class);
final Root<GoalMetricPeriodTotal> root = cq.from(GoalMetricPeriodTotal.class);
cq.select(cb.sum(root.get(GoalMetricPeriodTotal_.aggregatedValue))).where(
cb.equal(root.get(GoalMetricPeriodTotal_.athleteId), athleteId),
cb.equal(root.get(GoalMetricPeriodTotal_.metric), metric),
cb.equal(root.get(GoalMetricPeriodTotal_.isoWeekYear), isoWeekYear),
cb.equal(root.get(GoalMetricPeriodTotal_.calendarWeek), calendarWeek),
cb.equal(root.get(GoalMetricPeriodTotal_.year), year),
cb.equal(root.get(GoalMetricPeriodTotal_.month), month)
);
final Double result = em.createQuery(cq).getSingleResult();
return result != null ? result : 0.0;
}
public double sumByWeekAndYear(
final long athleteId,
final GoalMetricEnum metric,
final int isoWeekYear,
final int calendarWeek,
final int year) {
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<Double> cq = cb.createQuery(Double.class);
final Root<GoalMetricPeriodTotal> root = cq.from(GoalMetricPeriodTotal.class);
cq.select(cb.sum(root.get(GoalMetricPeriodTotal_.aggregatedValue))).where(
cb.equal(root.get(GoalMetricPeriodTotal_.athleteId), athleteId),
cb.equal(root.get(GoalMetricPeriodTotal_.metric), metric),
cb.equal(root.get(GoalMetricPeriodTotal_.isoWeekYear), isoWeekYear),
cb.equal(root.get(GoalMetricPeriodTotal_.calendarWeek), calendarWeek),
cb.equal(root.get(GoalMetricPeriodTotal_.year), year)
);
final Double result = em.createQuery(cq).getSingleResult();
return result != null ? result : 0.0;
}
public double sumByMonth(final long athleteId, final GoalMetricEnum metric, final int year, final int month) {
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<Double> cq = cb.createQuery(Double.class);

View File

@@ -34,7 +34,6 @@ public class GoalMetricPeriodTotalService extends AbstractService<GoalMetricPeri
return goalMetricPeriodTotalDAO;
}
@Transactional
public void recalculateIncremental(final long athleteId) {
for (final GoalMetricEnum metric : GoalMetricEnum.values()) {
recalculateMetricIncremental(athleteId, metric);
@@ -53,29 +52,34 @@ public class GoalMetricPeriodTotalService extends AbstractService<GoalMetricPeri
}
for (final IntervalsActivity activity : activities) {
final LocalDate date = activity.getStartTime().atZone(ZoneOffset.UTC).toLocalDate();
final int year = date.getYear();
final int month = date.getMonthValue();
final int calendarWeek = date.get(WeekFields.ISO.weekOfWeekBasedYear());
final int isoWeekYear = date.get(WeekFields.ISO.weekBasedYear());
final double value = intervalsActivityService.extractMetricValue(activity, metric);
recalculateMetric4Activity(athleteId, metric, activity);
}
}
final GoalMetricPeriodTotal existing = goalMetricPeriodTotalDAO.findByKey(athleteId, metric, year, month, calendarWeek);
if (existing != null) {
existing.setAggregatedValue(existing.getAggregatedValue() + value);
existing.setLastActivityId(activity.getId());
} else {
final GoalMetricPeriodTotal row = new GoalMetricPeriodTotal();
row.setAthleteId(athleteId);
row.setMetric(metric);
row.setYear(year);
row.setMonth(month);
row.setCalendarWeek(calendarWeek);
row.setIsoWeekYear(isoWeekYear);
row.setAggregatedValue(value);
row.setLastActivityId(activity.getId());
insert(row);
}
@Transactional
public void recalculateMetric4Activity(final long athleteId, final GoalMetricEnum metric, final IntervalsActivity activity) {
final LocalDate date = activity.getStartTime().atZone(ZoneOffset.UTC).toLocalDate();
final int year = date.getYear();
final int month = date.getMonthValue();
final int calendarWeek = date.get(WeekFields.ISO.weekOfWeekBasedYear());
final int isoWeekYear = date.get(WeekFields.ISO.weekBasedYear());
final double value = intervalsActivityService.extractMetricValue(activity, metric);
final GoalMetricPeriodTotal existing = goalMetricPeriodTotalDAO.findByKey(athleteId, metric, year, month, calendarWeek);
if (existing != null) {
existing.setAggregatedValue(existing.getAggregatedValue() + value);
existing.setLastActivityId(activity.getId());
} else {
final GoalMetricPeriodTotal row = new GoalMetricPeriodTotal();
row.setAthleteId(athleteId);
row.setMetric(metric);
row.setYear(year);
row.setMonth(month);
row.setCalendarWeek(calendarWeek);
row.setIsoWeekYear(isoWeekYear);
row.setAggregatedValue(value);
row.setLastActivityId(activity.getId());
insert(row);
}
}
@@ -84,6 +88,27 @@ public class GoalMetricPeriodTotalService extends AbstractService<GoalMetricPeri
return goalMetricPeriodTotalDAO.sumByWeek(athleteId, metric, isoWeekYear, calendarWeek);
}
@Transactional
public double getMonthlyTotalForWeek(
final long athleteId,
final GoalMetricEnum metric,
final int isoWeekYear,
final int calendarWeek,
final int year,
final int month) {
return goalMetricPeriodTotalDAO.sumByWeekAndMonth(athleteId, metric, isoWeekYear, calendarWeek, year, month);
}
@Transactional
public double getYearlyTotalForWeek(
final long athleteId,
final GoalMetricEnum metric,
final int isoWeekYear,
final int calendarWeek,
final int year) {
return goalMetricPeriodTotalDAO.sumByWeekAndYear(athleteId, metric, isoWeekYear, calendarWeek, year);
}
@Transactional
public double getMonthlyTotal(final long athleteId, final GoalMetricEnum metric, final int year, final int month) {
return goalMetricPeriodTotalDAO.sumByMonth(athleteId, metric, year, month);

View File

@@ -18,7 +18,7 @@ import {useDashboardGetDashboard} from '@/api/hooks'
import type {ActivitySummaryDo} from '@/types/ActivitySummaryDo'
import type {GoalDo} from '@/types/GoalDo'
import type {GoalMetricEnum} from '@/types/GoalMetricEnum'
import type {GoalMetricPeriodTotalDo} from '@/types/GoalMetricPeriodTotalDo'
import type {GoalMetricTotalsDo} from '@/types/GoalMetricTotalsDo'
import type {GoalPeriodEnum} from '@/types/GoalPeriodEnum'
import {formatDistance, formatDuration, sportIcon, summaryNum} from '@/utils/activity'
import {ActivityOverviewCard} from '@/components/dashboard/ActivityOverviewCard'
@@ -70,17 +70,13 @@ function formatGoalValue(metric: Metric, value: number): string {
type Actuals = Record<Metric, number>
function buildActualsFromTotals(
totals: GoalMetricPeriodTotalDo[],
totals: GoalMetricTotalsDo[],
period: GoalPeriodEnum,
year: number,
calendarWeek: number,
): Actuals {
const relevant = totals.filter(
(t) => t.period === period && t.year === year && t.calendarWeek === calendarWeek,
)
const result: Actuals = {DISTANCE: 0, TSS: 0, ELEVATION: 0, TIME: 0}
for (const t of relevant) {
result[t.metric] = t.metric === 'DISTANCE' ? t.aggregatedValue / 1000 : t.aggregatedValue
for (const t of totals) {
const raw = period === 'WEEKLY' ? t.weeklyTotal : period === 'MONTHLY' ? t.monthlyTotal : t.yearlyTotal
result[t.metric] = t.metric === 'DISTANCE' ? raw / 1000 : raw
}
return result
}
@@ -428,7 +424,7 @@ export function Dashboard({athleteId}: { athleteId: number }) {
const {data: dashboardData, isLoading} = useDashboardGetDashboard(athleteId, week, year)
const activities = dashboardData?.activities ?? []
const goals = dashboardData?.goals ?? []
const goalMetricPeriodTotals = dashboardData?.goalMetricPeriodTotals ?? []
const goalMetricTotals = dashboardData?.goalMetricTotals ?? []
const weekActivityMapStreams = dashboardData?.weekActivityMapStreams ?? {}
const weekActivityStreams = dashboardData?.weekActivityStreams ?? {}
@@ -446,9 +442,9 @@ export function Dashboard({athleteId}: { athleteId: number }) {
const wEnd = endOfISOWeek(wStart)
const weekDays = eachDayOfInterval({start: wStart, end: wEnd})
const weekActuals = buildActualsFromTotals(goalMetricPeriodTotals, 'WEEKLY', year, week)
const monthActuals = buildActualsFromTotals(goalMetricPeriodTotals, 'MONTHLY', year, week)
const yearActuals = buildActualsFromTotals(goalMetricPeriodTotals, 'YEARLY', year, week)
const weekActuals = buildActualsFromTotals(goalMetricTotals, 'WEEKLY')
const monthActuals = buildActualsFromTotals(goalMetricTotals, 'MONTHLY')
const yearActuals = buildActualsFromTotals(goalMetricTotals, 'YEARLY')
const weeklyGoals = goals.filter((g) => g.period === 'WEEKLY')
const monthlyGoals = goals.filter((g) => g.period === 'MONTHLY')