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); 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. * Get training load for a date range.
*/ */
@ -276,4 +295,6 @@ public class AnalyticsController {
case UNKNOWN -> "Not enough data to calculate form status."; case UNKNOWN -> "Not enough data to calculate form status.";
}; };
} }
private record RebuildResponse(String message) {}
} }

View file

@ -13,10 +13,15 @@
<h1> <h1>
<i class="bi bi-award-fill" style="color: var(--accent-orange);"></i> Achievements <i class="bi bi-award-fill" style="color: var(--accent-orange);"></i> Achievements
</h1> </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"> <a href="/analytics" class="btn btn-outline-primary">
<i class="bi bi-arrow-left"></i> Back to Dashboard <i class="bi bi-arrow-left"></i> Back to Dashboard
</a> </a>
</div> </div>
</div>
<!-- Stats Summary --> <!-- Stats Summary -->
<div class="row g-3 mb-4"> <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) { function updateStats(achievements) {
// Earned count // Earned count
document.getElementById('earned-count').textContent = achievements.length; document.getElementById('earned-count').textContent = achievements.length;
@ -108,6 +142,8 @@
if (achievements.length > 0) { if (achievements.length > 0) {
const latest = new Date(achievements[0].earnedAt); const latest = new Date(achievements[0].earnedAt);
document.getElementById('latest-date').textContent = latest.toLocaleDateString(); document.getElementById('latest-date').textContent = latest.toLocaleDateString();
} else {
document.getElementById('latest-date').textContent = '-';
} }
// Completion percentage // Completion percentage
@ -217,6 +253,7 @@
window.location.href = '/auth/login'; window.location.href = '/auth/login';
return; return;
} }
document.getElementById('rebuild-achievements-btn').addEventListener('click', rebuildAchievements);
loadAchievements(); loadAchievements();
}); });
</script> </script>