feat(analytics): add manual achievement rebuild action

Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
Marcus Fihlon 2026-04-29 10:07:01 +02:00
parent 2c567a5e8e
commit 2ac3d82fda
Signed by: McPringle
GPG key ID: C6B7F469EE363E1F
2 changed files with 67 additions and 9 deletions

View file

@ -133,6 +133,25 @@ public class AnalyticsController {
return ResponseEntity.ok(achievements);
}
/**
* Rebuild achievements for the authenticated user.
*/
@PostMapping("/achievements/rebuild")
public ResponseEntity<RebuildResponse> rebuildAchievements(
@AuthenticationPrincipal UserDetails userDetails) {
UUID userId = getUserId(userDetails);
try {
achievementService.rebuildAchievementsForUser(userId);
return ResponseEntity.ok(new RebuildResponse("Achievements recalculated successfully"));
} catch (Exception e) {
log.error("Failed to rebuild achievements for user {}", userDetails.getUsername(), e);
return ResponseEntity.internalServerError()
.body(new RebuildResponse("Failed to recalculate achievements: " + e.getMessage()));
}
}
/**
* Get training load for a date range.
*/
@ -276,4 +295,6 @@ public class AnalyticsController {
case UNKNOWN -> "Not enough data to calculate form status.";
};
}
private record RebuildResponse(String message) {}
}

View file

@ -8,15 +8,20 @@
</head>
<body>
<div layout:fragment="content">
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>
<i class="bi bi-award-fill" style="color: var(--accent-orange);"></i> Achievements
</h1>
<a href="/analytics" class="btn btn-outline-primary">
<i class="bi bi-arrow-left"></i> Back to Dashboard
</a>
</div>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>
<i class="bi bi-award-fill" style="color: var(--accent-orange);"></i> Achievements
</h1>
<div class="d-flex gap-2">
<button id="rebuild-achievements-btn" type="button" class="btn btn-outline-secondary">
<i class="bi bi-arrow-repeat"></i> Recalculate
</button>
<a href="/analytics" class="btn btn-outline-primary">
<i class="bi bi-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<!-- Stats Summary -->
<div class="row g-3 mb-4">
@ -100,6 +105,35 @@
}
}
async function rebuildAchievements() {
const rebuildBtn = document.getElementById('rebuild-achievements-btn');
const originalContent = rebuildBtn.innerHTML;
rebuildBtn.disabled = true;
rebuildBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Recalculating...';
try {
const response = await FitPubAuth.authenticatedFetch('/api/analytics/achievements/rebuild', {
method: 'POST'
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Failed to recalculate achievements');
}
FitPub.showAlert('success', result.message || 'Achievements recalculated successfully');
await loadAchievements();
} catch (error) {
console.error('Error rebuilding achievements:', error);
FitPub.showAlert('danger', error.message || 'Failed to recalculate achievements');
} finally {
rebuildBtn.disabled = false;
rebuildBtn.innerHTML = originalContent;
}
}
function updateStats(achievements) {
// Earned count
document.getElementById('earned-count').textContent = achievements.length;
@ -108,6 +142,8 @@
if (achievements.length > 0) {
const latest = new Date(achievements[0].earnedAt);
document.getElementById('latest-date').textContent = latest.toLocaleDateString();
} else {
document.getElementById('latest-date').textContent = '-';
}
// Completion percentage
@ -217,6 +253,7 @@
window.location.href = '/auth/login';
return;
}
document.getElementById('rebuild-achievements-btn').addEventListener('click', rebuildAchievements);
loadAchievements();
});
</script>