<?php
namespace App\Controller;
use App\Entity\Device;
use App\Entity\Exercise;
use App\Entity\DataVersion;
use App\Entity\WorkoutData;
use App\Entity\WorkoutTemplate;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/admin')]
class DashboardController extends AbstractController
{
#[Route('/', name: 'app_dashboard')]
public function index(EntityManagerInterface $em): Response
{
$workouts = $em->getRepository(WorkoutData::class)
->createQueryBuilder('w')
->where('w.gymName IN (:gymNames)')
->setParameter('gymNames', [
'UPDATE_FITNESS_OSTERMUNDIGEN',
'UPDATE_FITNESS_MARKTGASSE',
'UPDATE_FITNESS_TRUE_MARKTGASSE',
'UPDATE_FITNESS_MUNCHWILEN',
])
->getQuery()
->getResult();
// Now $workoutDataByGymByDay includes *every* date from the earliest to latest
// for each gym, with missing days set to 0.
// dd($workoutDataByGymByDay);
return $this->render('dashboard/index.html.twig', [
'workout_templates' => [
'count' => $em->getRepository(WorkoutTemplate::class)->getEntriesCount(),
],
'exercises' => [
'count' => $em->getRepository(Exercise::class)->getEntriesCount(),
],
'gyms' => [
'count' => $em->getRepository(Device::class)->getEntriesCount(),
],
'dataVersion' => $em->getRepository(DataVersion::class)
->findOneBy([], ['id' => 'ASC'])
->getVersionCounter(),
'devices' => $em->getRepository(Device::class)->findAll(),
'workoutFrequencyByDayByGym' => $this->computeDailyWorkoutCountsByGym($workouts),
'workoutCount' => count($workouts),
'activityTime' => $this->computeTotalWorkoutTime($workouts),
'setsCount' => $this->computeTotalSets($workouts),
'repsCount' => $this->computeTotalRepetitions($workouts),
'avgBTScore' => $this->computeMedianRythmScore($workouts),
'rhythmScores' => $this->getAllRythmScores($workouts),
'moduleTypeRatios' => $this->computeModuleTypeFrequencies($workouts),
'timeToStartPoseByExercise' => $this->computeAverageTimeToStartPoseByExercise($workouts),
'rhythmScoreByExercise' => $this->computeAverageRythmScoreByExercise($workouts),
'workoutTemplateFrequency' => $this->computeWorkoutsPerTemplate($workouts, $em),
]);
}
private function computeDailyWorkoutCountsByGym(array $workouts): array
{
$validGyms = [
'UPDATE_FITNESS_OSTERMUNDIGEN',
'UPDATE_FITNESS_MARKTGASSE',
'UPDATE_FITNESS_TRUE_MARKTGASSE',
'UPDATE_FITNESS_MUNCHWILEN',
];
$earliestTimestamp = null;
foreach ($workouts as $workout) {
$start = $workout->getStartTimestamp();
if ($start !== null && $start > 0) {
if ($earliestTimestamp === null || $start < $earliestTimestamp) {
$earliestTimestamp = $start;
}
}
}
if ($earliestTimestamp === null) {
return [];
}
$startDate = (new \DateTime())->setTimestamp((int) ($earliestTimestamp / 1000))->setTime(0, 0, 0);
$today = new \DateTime('today');
$result = [];
$currentDate = clone $startDate;
$workoutCounts = [];
foreach ($workouts as $workout) {
$start = $workout->getStartTimestamp();
$gymName = $workout->getGymName();
if ($start === null || $start == 0 || !in_array($gymName, $validGyms)) {
continue;
}
$date = (new \DateTime())->setTimestamp((int) ($start / 1000))->format('d.m.Y');
if (!isset($workoutCounts[$date])) {
$workoutCounts[$date] = array_fill_keys($validGyms, 0);
}
$workoutCounts[$date][$gymName]++;
}
while ($currentDate <= $today) {
$dayKey = $currentDate->format('d.m.Y');
$counts = $workoutCounts[$dayKey] ?? array_fill_keys($validGyms, 0);
$result[] = array_merge(['day' => $dayKey], $counts);
$currentDate->modify('+1 day');
}
return $result;
}
function getAllRythmScores(array $workouts): array
{
$rythmScores = [];
// Iterate over each workout
foreach ($workouts as $workout) {
/** @var WorkoutData $workout */
$jsonData = $workout->getJsonData();
// Skip if jsonData is null or empty
if ($jsonData === null || $jsonData === '') {
continue;
}
// Decode JSON string to array
$decodedData = json_decode($jsonData, true); // true = return as array
// Check if decoding succeeded and setData exists
if ($decodedData === null || !isset($decodedData['setData']) || !is_array($decodedData['setData'])) {
continue;
}
// Collect rythmScore values from setData
foreach ($decodedData['setData'] as $set) {
if (isset($set['rythmScore']) && is_numeric($set['rythmScore'])) {
$rythmScores[] = (float) $set['rythmScore'] * 100; // Cast to float for consistency
}
}
}
return $rythmScores;
}
function computeTotalWorkoutTime($workouts): string
{
$totalMilliseconds = 0;
// Iterate over each workout
foreach ($workouts as $workout) {
/** @var WorkoutData $workout */
$start = $workout->getStartTimestamp();
$end = $workout->getEndTimestamp();
// Skip if timestamps are null or 0
if ($start === null || $end === null || $start == 0 || $end == 0) {
continue;
}
// Calculate difference in milliseconds
$diff = $end - $start;
if ($diff > 0) { // Ensure end is after start
$totalMilliseconds += $diff;
}
}
// Convert milliseconds to seconds
$totalSeconds = $totalMilliseconds / 1000;
// Convert total seconds to hours and minutes
$hours = floor($totalSeconds / 3600); // 3600 seconds = 1 hour
$remainingSeconds = $totalSeconds % 3600;
$minutes = floor($remainingSeconds / 60);
// Return formatted string
return sprintf('%dh %dm', $hours, $minutes);
}
function computeWorkoutMinutes(array $workouts): array
{
$minutesArray = [];
// Iterate over each workout
foreach ($workouts as $workout) {
/** @var WorkoutData $workout */
$start = $workout->getStartTimestamp();
$end = $workout->getEndTimestamp();
// Skip if timestamps are null or 0
if ($start === null || $end === null || $start == 0 || $end == 0) {
continue;
}
// Calculate difference in milliseconds
$diff = $end - $start;
if ($diff > 0) { // Ensure end is after start
// Convert milliseconds to minutes (1 min = 60,000 ms)
$minutes = $diff / 60000;
$minutesArray[] = $minutes;
}
}
return $minutesArray;
}
function computeTotalSets(array $workouts): int
{
$totalSets = 0;
// Iterate over each workout
foreach ($workouts as $workout) {
/** @var WorkoutData $workout */
$jsonData = $workout->getJsonData();
// Skip if jsonData is null or empty
if ($jsonData === null || $jsonData === '') {
continue;
}
// Decode JSON string to array/object
$decodedData = json_decode($jsonData, true); // true = return as array
// Check if decoding succeeded and setData exists
if ($decodedData === null || !isset($decodedData['setData']) || !is_array($decodedData['setData'])) {
continue;
}
// Add the number of sets (length of setData array)
$totalSets += count($decodedData['setData']);
}
return $totalSets;
}
function computeTotalRepetitions(array $workouts): int
{
$totalReps = 0;
// Iterate over each workout
foreach ($workouts as $workout) {
/** @var WorkoutData $workout */
$jsonData = $workout->getJsonData();
// Skip if jsonData is null or empty
if ($jsonData === null || $jsonData === '') {
continue;
}
// Decode JSON string to array
$decodedData = json_decode($jsonData, true); // true = return as array
// Check if decoding succeeded and setData exists
if ($decodedData === null || !isset($decodedData['setData']) || !is_array($decodedData['setData'])) {
continue;
}
// Iterate over setData array and sum actualReps
foreach ($decodedData['setData'] as $set) {
if (isset($set['actualReps']) && is_numeric($set['actualReps']) && isset($set['targetReps']) && is_numeric($set['targetReps']) && $set['targetReps'] > 1) {
$totalReps += (int) $set['actualReps']; // Cast to int for safety
}
}
}
return $totalReps;
}
function computeMedianBtScore(array $workouts): ?float
{
$scores = [];
// Collect all btScore values
foreach ($workouts as $workout) {
/** @var WorkoutData $workout */
$btScore = $workout->getBtScore();
// Skip if btScore is null or not numeric
if ($btScore === null || !is_numeric($btScore)) {
continue;
}
$scores[] = (float) $btScore; // Cast to float for consistency
}
// If no valid scores, return null
if (empty($scores)) {
return null;
}
// Sort scores in ascending order
sort($scores);
$count = count($scores);
// Calculate median
if ($count % 2 === 0) {
// Even number of scores: average the two middle values
$middleIndex = $count / 2;
return ($scores[$middleIndex - 1] + $scores[$middleIndex]) / 2;
} else {
// Odd number of scores: take the middle value
$middleIndex = floor($count / 2);
return $scores[$middleIndex];
}
}
private function computeWorkoutsPerTemplate(array $workouts, EntityManagerInterface $em): array
{
$templates = $em->getRepository(WorkoutTemplate::class)->findAll();
$templateMap = [];
foreach ($templates as $template) {
$templateMap[$template->getId()] = $template->getName();
}
$workoutCounts = array_fill_keys($templateMap, 0);
foreach ($workouts as $workout) {
$jsonData = $workout->getJsonData();
if ($jsonData === null || $jsonData === '') {
continue;
}
$decodedData = json_decode($jsonData, true);
if (
$decodedData === null ||
!isset($decodedData['workoutCloudId']) || !is_numeric($decodedData['workoutCloudId'])
) {
continue;
}
$templateId = (int) $decodedData['workoutCloudId'];
if (!isset($templateMap[$templateId])) {
continue;
}
$templateName = $templateMap[$templateId];
$workoutCounts[$templateName]++;
}
return $workoutCounts;
}
function computeMedianRythmScore(array $workouts): ?float
{
$rythmScores = [];
// Iterate over each workout
foreach ($workouts as $workout) {
/** @var WorkoutData $workout */
$jsonData = $workout->getJsonData();
// Skip if jsonData is null or empty
if ($jsonData === null || $jsonData === '') {
continue;
}
// Decode JSON string to array
$decodedData = json_decode($jsonData, true); // true = return as array
// Check if decoding succeeded and setData exists
if ($decodedData === null || !isset($decodedData['setData']) || !is_array($decodedData['setData'])) {
continue;
}
// Collect rythmScore values from setData
foreach ($decodedData['setData'] as $set) {
if (isset($set['rythmScore']) && is_numeric($set['rythmScore'])) {
$rythmScores[] = (float) $set['rythmScore']; // Cast to float for consistency
}
}
}
// If no valid rythmScores, return null
if (empty($rythmScores)) {
return null;
}
// Sort scores in ascending order
sort($rythmScores);
$count = count($rythmScores);
// Calculate median
if ($count % 2 === 0) {
// Even number of scores: average the two middle values
$middleIndex = $count / 2;
return ($rythmScores[$middleIndex - 1] + $rythmScores[$middleIndex]) / 2;
} else {
// Odd number of scores: take the middle value
$middleIndex = floor($count / 2);
return $rythmScores[$middleIndex];
}
}
function computeModuleTypeFrequencies(array $workouts): array
{
// Initialize frequency array for moduleTypes 0, 1, 2, 3
$frequencies = [
0 => 0,
1 => 0,
2 => 0,
3 => 0,
];
// Iterate over each workout
foreach ($workouts as $workout) {
/** @var WorkoutData $workout */
$moduleType = $workout->getModuleType();
// Only count if moduleType is valid (0, 1, 2, or 3)
if ($moduleType !== null && array_key_exists($moduleType, $frequencies)) {
$frequencies[$moduleType]++;
}
}
$freqs = [];
$freqs[] = [
'name' => 'Guided Workouts',
'value' => $frequencies[0]
];
$freqs[] = [
'name' => 'Learn Exercises',
'value' => $frequencies[1]
];
$freqs[] = [
'name' => 'Fitness Challenges',
'value' => $frequencies[2]
];
$freqs[] = [
'name' => 'Warm Up',
'value' => $frequencies[3]
];
return $freqs;
}
private function computeAverageRythmScoreByExercise(array $workouts): array
{
$exerciseStats = [];
foreach ($workouts as $workout) {
$jsonData = $workout->getJsonData();
if ($jsonData === null || $jsonData === '') {
continue;
}
$decodedData = json_decode($jsonData, true);
if ($decodedData === null || !isset($decodedData['setData']) || !is_array($decodedData['setData'])) {
continue;
}
foreach ($decodedData['setData'] as $set) {
if (
isset($set['exerciseName']) && is_string($set['exerciseName']) &&
isset($set['rythmScore']) && is_numeric($set['rythmScore'])
) {
$exerciseName = $set['exerciseName'];
$rythmScore = (float) $set['rythmScore'];
if ($rythmScore < 0.1) continue;
if (!isset($exerciseStats[$exerciseName])) {
$exerciseStats[$exerciseName] = ['sum' => 0, 'count' => 0];
}
$exerciseStats[$exerciseName]['sum'] += $rythmScore * 100;
$exerciseStats[$exerciseName]['count']++;
}
}
}
$result = [];
foreach ($exerciseStats as $exerciseName => $stats) {
if ($stats['count'] > 0) {
$result[$exerciseName] = $stats['sum'] / $stats['count'];
}
}
return $result;
}
private function computeAverageTimeToStartPoseByExercise(array $workouts): array
{
$exerciseStats = [];
foreach ($workouts as $workout) {
$jsonData = $workout->getJsonData();
if ($jsonData === null || $jsonData === '') {
continue;
}
$decodedData = json_decode($jsonData, true);
if ($decodedData === null || !isset($decodedData['setData']) || !is_array($decodedData['setData'])) {
continue;
}
foreach ($decodedData['setData'] as $set) {
if (
isset($set['exerciseName']) && is_string($set['exerciseName']) &&
isset($set['timeToStartPose']) && is_numeric($set['timeToStartPose'])
) {
$exerciseName = $set['exerciseName'];
$timeToStartPose = (float) $set['timeToStartPose'] / 1000;
if ($timeToStartPose > 30) continue;
if (!isset($exerciseStats[$exerciseName])) {
$exerciseStats[$exerciseName] = ['sum' => 0, 'count' => 0];
}
$exerciseStats[$exerciseName]['sum'] += $timeToStartPose;
$exerciseStats[$exerciseName]['count']++;
}
}
}
$result = [];
foreach ($exerciseStats as $exerciseName => $stats) {
if ($stats['count'] > 0) {
$result[$exerciseName] = $stats['sum'] / $stats['count'];
}
}
return $result;
}
}