This commit is contained in:
Bastian Wagner
2026-06-19 16:30:30 +02:00
parent 8e9f498d90
commit d8b1a47a2a
6 changed files with 789 additions and 3 deletions

View File

@@ -148,6 +148,92 @@
}
</div>
</div>
@if (data.performance && data.performance.length > 0) {
<div class="panel">
<div class="section-title">
<h2>Leistungskennzahlen</h2>
</div>
<div class="performance-grid">
@for (kpi of data.performance; track kpi.key) {
<div class="kpi-card">
<span class="kpi-label">{{ kpi.label }}</span>
<strong class="kpi-value">{{ formatPerformanceKpi(kpi) }}</strong>
<small class="kpi-desc">{{ kpi.description }}</small>
</div>
}
</div>
</div>
}
@if (data.injuryRisk) {
<div class="panel">
<div class="section-title">
<h2>Verletzungsrisiko</h2>
</div>
<div class="risk-display">
<div class="risk-badge" [class.risk-low]="data.injuryRisk.riskLevel === 'low'"
[class.risk-medium]="data.injuryRisk.riskLevel === 'medium'"
[class.risk-high]="data.injuryRisk.riskLevel === 'high'">
{{ data.injuryRisk.riskLevel | uppercase }}
</div>
<div class="risk-meter">
<div class="risk-fill" [style.width.%]="data.injuryRisk.score"></div>
</div>
<p class="risk-recommendation">{{ data.injuryRisk.recommendation }}</p>
</div>
</div>
}
@if (data.readiness) {
<div class="panel">
<div class="section-title">
<h2>Bereitschaft</h2>
</div>
<div class="readiness-display">
<div class="readiness-badge" [class.status-ready]="data.readiness.status === 'ready'"
[class.status-cautious]="data.readiness.status === 'cautious'"
[class.status-rest]="data.readiness.status === 'rest'">
{{ data.readiness.score }} / 100
</div>
<div class="readiness-status">{{ data.readiness.status | uppercase }}</div>
<div class="readiness-reasons">
@for (reason of data.readiness.reasons; track reason) {
<span class="reason-tag">{{ reason }}</span>
}
</div>
</div>
</div>
}
@if (data.predictions) {
<div class="panel">
<div class="section-title">
<h2>Vorhersagen</h2>
</div>
<div class="predictions-grid">
<div class="prediction-card">
<h3>VO2max</h3>
<strong>{{ data.predictions.vo2max ?? '-' }} ml/kg/min</strong>
<small>Schatzung der aeroben Kapazitat</small>
</div>
<div class="prediction-card">
<h3>Nachster Meilenstein</h3>
<strong>{{ data.predictions.nextMilestone.target }}</strong>
<small>{{ data.predictions.nextMilestone.type }}</small>
</div>
</div>
<div class="race-times">
<h3>Geschatzte Rennzeiten</h3>
<div class="race-grid">
<div><span>5 km:</span><strong>{{ formatRaceTime(data.predictions.raceTimes.distance5k) }}</strong></div>
<div><span>10 km:</span><strong>{{ formatRaceTime(data.predictions.raceTimes.distance10k) }}</strong></div>
<div><span>Halbmarathon:</span><strong>{{ formatRaceTime(data.predictions.raceTimes.distanceHalfMarathon) }}</strong></div>
<div><span>Marathon:</span><strong>{{ formatRaceTime(data.predictions.raceTimes.distanceMarathon) }}</strong></div>
</div>
</div>
</div>
}
}
}
</section>

View File

@@ -234,7 +234,10 @@ a.pr-row:hover,
.kpis,
.chart-grid,
.progression-grid {
.progression-grid,
.performance-grid,
.predictions-grid,
.race-grid {
grid-template-columns: 1fr;
}
@@ -242,3 +245,259 @@ a.pr-row:hover,
min-width: 0;
}
}
/* Performance KPIs Grid */
.performance-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 14px;
}
.kpi-card {
border: 1px solid #eef2f7;
border-radius: 6px;
padding: 16px;
display: grid;
gap: 6px;
}
.kpi-label {
color: #687386;
font-size: 0.82rem;
font-weight: 700;
}
.kpi-value {
display: block;
font-size: 1.25rem;
margin-top: 4px;
}
.kpi-desc {
color: #687386;
font-size: 0.75rem;
}
/* Risk Display */
.risk-display {
display: grid;
gap: 16px;
margin-top: 14px;
}
.risk-badge {
display: inline-block;
padding: 6px 16px;
border-radius: 20px;
font-weight: 800;
font-size: 0.82rem;
text-align: center;
}
.risk-low {
background: #d1f5ed;
color: #1c8b76;
}
.risk-medium {
background: #f5e6b8;
color: #d2a21f;
}
.risk-high {
background: #fdeded;
color: #d63f4c;
}
.risk-meter {
background: #eef2f7;
border-radius: 999px;
height: 12px;
overflow: hidden;
}
.risk-fill {
border-radius: inherit;
height: 100%;
background: linear-gradient(90deg, #d63f4c, #d2a21f, #1c8b76);
}
.risk-recommendation {
color: #4e5a6b;
font-size: 0.9rem;
margin: 0;
}
.risk-factors {
display: grid;
gap: 8px;
grid-template-columns: repeat(3, 1fr);
margin-top: 12px;
}
.risk-factors div {
display: flex;
justify-content: space-between;
}
.risk-factors span {
color: #687386;
font-size: 0.82rem;
}
.risk-factors strong {
color: #4e5a6b;
}
/* Readiness Display */
.readiness-display {
display: grid;
gap: 16px;
margin-top: 14px;
}
.readiness-badge {
display: inline-block;
padding: 8px 20px;
border-radius: 50%;
font-weight: 800;
font-size: 1.1rem;
text-align: center;
}
.status-ready {
background: #d1f5ed;
color: #1c8b76;
}
.status-cautious {
background: #f5e6b8;
color: #d2a21f;
}
.status-rest {
background: #fdeded;
color: #d63f4c;
}
.readiness-status {
font-weight: 700;
font-size: 1.1rem;
text-transform: uppercase;
}
.readiness-reasons {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.reason-tag {
background: #eef2f7;
border-radius: 12px;
color: #4e5a6b;
font-size: 0.82rem;
padding: 4px 12px;
}
.readiness-factors {
display: grid;
gap: 8px;
grid-template-columns: repeat(3, 1fr);
margin-top: 12px;
}
.readiness-factors div {
display: flex;
justify-content: space-between;
}
.readiness-factors span {
color: #687386;
font-size: 0.82rem;
}
.readiness-factors strong {
color: #4e5a6b;
}
/* Predictions */
.predictions-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 14px;
}
.prediction-card {
border: 1px solid #eef2f7;
border-radius: 6px;
padding: 16px;
display: grid;
gap: 8px;
}
.prediction-card h3 {
color: #4e5a6b;
font-size: 0.9rem;
margin: 0;
}
.prediction-card strong {
display: block;
font-size: 1.25rem;
}
.prediction-card small {
color: #687386;
font-size: 0.82rem;
}
.race-times {
margin-top: 18px;
}
.race-times h3 {
color: #4e5a6b;
font-size: 0.9rem;
margin: 0 0 12px;
}
.race-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.race-grid div {
border: 1px solid #eef2f7;
border-radius: 6px;
padding: 12px;
display: flex;
justify-content: space-between;
}
.race-grid span {
color: #687386;
font-size: 0.82rem;
}
.race-grid strong {
color: #4e5a6b;
font-size: 1rem;
}
@media (max-width: 980px) {
.performance-grid,
.predictions-grid,
.race-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.risk-factors,
.readiness-factors {
grid-template-columns: 1fr;
}
}

View File

@@ -23,6 +23,10 @@ import {
} from './running-format';
import {
RunningKpiDashboard,
RunningPerformanceKpi,
RunningInjuryRisk,
RunningReadinessScore,
RunningPredictions,
RunningProgressionMetric,
} from './running.types';
import { RunningService } from './running.service';
@@ -153,6 +157,51 @@ export class RunningKpiDashboardComponent implements OnInit, OnDestroy {
protected pace = pace;
protected shortDate = shortDate;
protected formatPerformanceKpi(kpi: RunningPerformanceKpi): string {
if (kpi.value === null) return '-';
switch (kpi.key) {
case 'avgPace':
return pace(kpi.value);
case 'longestRun':
return `${(kpi.value / 1000).toFixed(1)} km`;
case 'streakDays':
case 'consistencyScore':
case 'elevationPerKm':
case 'vo2max':
return `${kpi.value} ${kpi.unit}`;
default:
return `${kpi.value} ${kpi.unit}`;
}
}
protected getTrendClass(trend: RunningPerformanceKpi['trend']): string {
switch (trend) {
case 'up': return 'positive';
case 'down': return 'negative';
case 'stable': return 'neutral';
default: return '';
}
}
protected getRiskLevelClass(riskLevel: RunningInjuryRisk['riskLevel']): string {
switch (riskLevel) {
case 'low': return 'risk-low';
case 'medium': return 'risk-medium';
case 'high': return 'risk-high';
default: return '';
}
}
protected getReadinessStatusClass(status: RunningReadinessScore['status']): string {
switch (status) {
case 'ready': return 'status-ready';
case 'cautious': return 'status-cautious';
case 'rest': return 'status-rest';
default: return '';
}
}
private renderCharts(): void {
const data = this.kpis();
if (!data) {

View File

@@ -134,4 +134,55 @@ export interface RunningKpiDashboard {
intensityDistribution: RunningIntensityDistributionBucket[];
progression: RunningProgressionMetric[];
personalRecords: RunningPersonalRecordEstimate[];
performance: RunningPerformanceKpi[];
injuryRisk: RunningInjuryRisk | null;
readiness: RunningReadinessScore | null;
predictions: RunningPredictions | null;
}
export interface RunningPerformanceKpi {
key: string;
label: string;
value: number | null;
unit: string;
trend: 'up' | 'down' | 'stable' | null;
description: string;
}
export interface RunningInjuryRisk {
riskLevel: 'low' | 'medium' | 'high';
score: number;
factors: {
load: number;
recovery: number;
intensity: number;
};
recommendation: string;
}
export interface RunningReadinessScore {
score: number;
status: 'ready' | 'cautious' | 'rest';
factors: {
form: number;
fatigue: number;
recovery: number;
};
reasons: string[];
}
export interface RunningPredictions {
raceTimes: {
distance5k: number | null;
distance10k: number | null;
distanceHalfMarathon: number | null;
distanceMarathon: number | null;
};
nextMilestone: {
type: string;
target: string;
predictedDate: string | null;
confidence: number;
};
vo2max: number | null;
}