Dashboard

This commit is contained in:
2026-04-13 19:43:34 +02:00
parent ce706e8a46
commit 1184abcf69
7 changed files with 57 additions and 33 deletions

View File

@@ -57,18 +57,35 @@ public class GoalMetricPeriodTotalDAO extends AbstractDAO<GoalMetricPeriodTotal>
return em.createQuery(cq).setMaxResults(1).getResultStream().findFirst().orElse(null);
}
public List<GoalMetricPeriodTotal> findByAthleteAndWeek(
public List<GoalMetricPeriodTotal> findByAthleteForDashboard(
final long athleteId,
final int year,
final int calendarWeek) {
final int weekYear,
final int weekIndex,
final int monthYear,
final int monthIndex,
final int calendarYear) {
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<GoalMetricPeriodTotal> cq = cb.createQuery(GoalMetricPeriodTotal.class);
final Root<GoalMetricPeriodTotal> root = cq.from(GoalMetricPeriodTotal.class);
cq.select(root).where(
cb.equal(root.get(GoalMetricPeriodTotal_.athleteId), athleteId),
cb.equal(root.get(GoalMetricPeriodTotal_.period), GoalPeriodEnum.WEEKLY),
cb.equal(root.get(GoalMetricPeriodTotal_.periodYear), year),
cb.equal(root.get(GoalMetricPeriodTotal_.periodIndex), calendarWeek)
cb.or(
cb.and(
cb.equal(root.get(GoalMetricPeriodTotal_.period), GoalPeriodEnum.WEEKLY),
cb.equal(root.get(GoalMetricPeriodTotal_.periodYear), weekYear),
cb.equal(root.get(GoalMetricPeriodTotal_.periodIndex), weekIndex)
),
cb.and(
cb.equal(root.get(GoalMetricPeriodTotal_.period), GoalPeriodEnum.MONTHLY),
cb.equal(root.get(GoalMetricPeriodTotal_.periodYear), monthYear),
cb.equal(root.get(GoalMetricPeriodTotal_.periodIndex), monthIndex)
),
cb.and(
cb.equal(root.get(GoalMetricPeriodTotal_.period), GoalPeriodEnum.YEARLY),
cb.equal(root.get(GoalMetricPeriodTotal_.periodYear), calendarYear),
cb.equal(root.get(GoalMetricPeriodTotal_.periodIndex), 1)
)
)
);
return em.createQuery(cq).getResultList();
}

View File

@@ -69,7 +69,15 @@ public class GoalMetricPeriodTotalService extends AbstractService<GoalMetricPeri
@Transactional
public List<GoalMetricPeriodTotal> getByAthleteAndWeek(final long athleteId, final int year, final int calendarWeek) {
return goalMetricAggregateDAO.findByAthleteAndWeek(athleteId, year, calendarWeek);
final LocalDate monday = LocalDate.of(year, 1, 4)
.with(WeekFields.ISO.weekOfWeekBasedYear(), calendarWeek)
.with(WeekFields.ISO.dayOfWeek(), 1);
return goalMetricAggregateDAO.findByAthleteForDashboard(
athleteId,
year, calendarWeek,
monday.getYear(), monday.getMonthValue(),
monday.getYear()
);
}
private List<IntervalsActivity> fetchWeekActivities(final long athleteId, final int year, final int calendarWeek) {

View File

@@ -2,14 +2,12 @@ import {
addWeeks,
eachDayOfInterval,
endOfISOWeek,
endOfMonth,
format,
getISOWeek,
getISOWeekYear,
isSameDay,
isToday,
startOfISOWeek,
startOfMonth,
} from 'date-fns'
import {ChevronLeft, ChevronRight} from 'lucide-react'
import {useSearchParams} from 'react-router-dom'
@@ -19,6 +17,8 @@ import {useDashboardGetDashboard} from '@/api/hooks'
import type {IntervalsActivityDo} from '@/types/IntervalsActivityDo'
import type {GoalDo} from '@/types/GoalDo'
import type {GoalMetricEnum} from '@/types/GoalMetricEnum'
import type {GoalMetricPeriodTotalDo} from '@/types/GoalMetricPeriodTotalDo'
import type {GoalPeriodEnum} from '@/types/GoalPeriodEnum'
import {formatDistance, formatDuration, sportIcon} from '@/utils/activity'
// ── Week helpers ──────────────────────────────────────────────────────────────
@@ -102,13 +102,20 @@ function formatGoalValue(metric: Metric, value: number): string {
}
}
function computeActuals(acts: IntervalsActivityDo[]): Actuals {
return {
DISTANCE: acts.reduce((s, a) => s + a.distanceM, 0) / 1000,
TSS: acts.reduce((s, a) => s + (a.trainingLoad ?? 0), 0),
ELEVATION: acts.reduce((s, a) => s + (a.totalElevationGainM ?? 0), 0),
TIME: acts.reduce((s, a) => s + a.movingTimeS, 0),
function buildActualsFromTotals(
totals: GoalMetricPeriodTotalDo[],
period: GoalPeriodEnum,
periodYear: number,
periodIndex: number,
): Actuals {
const relevant = totals.filter(
(t) => t.period === period && t.periodYear === periodYear && t.periodIndex === periodIndex,
)
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
}
return result
}
// ── GoalRow ───────────────────────────────────────────────────────────────────
@@ -334,6 +341,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 ?? []
if (isLoading) {
return (
@@ -351,23 +359,9 @@ export function Dashboard({athleteId}: { athleteId: number }) {
// ── Actuals ─────────────────────────────────────────────────────────────────
const mStart = startOfMonth(wStart)
const mEnd = endOfMonth(wStart)
const yStart = new Date(wStart.getFullYear(), 0, 1)
const yEnd = new Date(wStart.getFullYear(), 11, 31, 23, 59, 59)
const weekActuals = computeActuals(activities.filter((a) => {
const t = new Date(a.startTime);
return t >= wStart && t <= wEnd
}))
const monthActuals = computeActuals(activities.filter((a) => {
const t = new Date(a.startTime);
return t >= mStart && t <= mEnd
}))
const yearActuals = computeActuals(activities.filter((a) => {
const t = new Date(a.startTime);
return t >= yStart && t <= yEnd
}))
const weekActuals = buildActualsFromTotals(goalMetricPeriodTotals, 'WEEKLY', year, week)
const monthActuals = buildActualsFromTotals(goalMetricPeriodTotals, 'MONTHLY', wStart.getFullYear(), wStart.getMonth() + 1)
const yearActuals = buildActualsFromTotals(goalMetricPeriodTotals, 'YEARLY', wStart.getFullYear(), 1)
const weeklyGoals = goals.filter((g) => g.period === 'WEEKLY')
const monthlyGoals = goals.filter((g) => g.period === 'MONTHLY')

View File

@@ -1,6 +1,8 @@
// GENERATED — do not edit manually.
import type { AbstractTimestampedEntityDo } from './AbstractTimestampedEntityDo';
import type { GoalPeriodEnum } from './GoalPeriodEnum';
import type { GoalMetricEnum } from './GoalMetricEnum';
export interface GoalDo extends AbstractTimestampedEntityDo {
athleteId: number;

View File

@@ -1,6 +1,8 @@
// GENERATED — do not edit manually.
import type { AbstractTimestampedEntityDo } from './AbstractTimestampedEntityDo';
import type { GoalMetricEnum } from './GoalMetricEnum';
import type { GoalPeriodEnum } from './GoalPeriodEnum';
export interface GoalMetricPeriodTotalDo extends AbstractTimestampedEntityDo {
athleteId: number;

View File

@@ -210,6 +210,7 @@ public class TypeScriptClientGeneratorMojo extends AbstractMojo {
if (!cls.getName().startsWith(BACKEND_JAVA_PREFIX)) return false;
if (cls.isAnnotation() || cls.isSynthetic() || cls.isInterface()) return false;
if (cls.getEnclosingClass() != null) return false;
if (cls.isEnum()) return true; // enums with abstract methods are still domain types
return !Modifier.isAbstract(cls.getModifiers());
}

View File

@@ -1,5 +1,5 @@
com/peakup/maven/TypeScriptClientGeneratorMojo.class
com/peakup/maven/TypeScriptClientGeneratorMojo$ResourceInfo.class
com/peakup/maven/PackageInfoGeneratorMojo.class
com/peakup/maven/TypeScriptClientGeneratorMojo$ParamInfo.class
com/peakup/maven/TypeScriptClientGeneratorMojo$ResourceInfo.class
com/peakup/maven/TypeScriptClientGeneratorMojo$EndpointInfo.class