} */ public static function forUser(User $user): array { $completedDates = WorkoutSession::where('user_id', $user->id) ->whereNotNull('completed_at') ->pluck('completed_at') ->map(fn ($dt) => $dt->toDateString()) ->unique() ->sort() ->values(); $currentStreak = self::currentStreak($completedDates); $longestStreak = self::longestStreak($completedDates); $completedSessions = $completedDates->isNotEmpty() ? WorkoutSession::where('user_id', $user->id)->whereNotNull('completed_at')->count() : 0; $hasCenturyLift = WorkoutSessionItem::whereHas('session', fn ($q) => $q->where('user_id', $user->id)) ->where('done', true) ->where('actual_weight', '>=', 100) ->exists(); $badges = [ [ 'key' => 'first_steps', 'name' => 'First Steps', 'description' => 'Complete your first workout session.', 'earned' => $completedSessions >= 1, ], [ 'key' => 'consistent', 'name' => 'Consistent', 'description' => 'Complete 10 workout sessions.', 'earned' => $completedSessions >= 10, ], [ 'key' => 'dedicated', 'name' => 'Dedicated', 'description' => 'Complete 50 workout sessions.', 'earned' => $completedSessions >= 50, ], [ 'key' => 'week_warrior', 'name' => 'Week Warrior', 'description' => 'Reach a 7-day training streak.', 'earned' => $longestStreak >= 7, ], [ 'key' => 'iron_will', 'name' => 'Iron Will', 'description' => 'Reach a 30-day training streak.', 'earned' => $longestStreak >= 30, ], [ 'key' => 'century_club', 'name' => 'Century Club', 'description' => 'Log 100kg or more on a completed set.', 'earned' => $hasCenturyLift, ], ]; return [ 'current_streak' => $currentStreak, 'longest_streak' => $longestStreak, 'badges' => $badges, ]; } /** * @param \Illuminate\Support\Collection $completedDates Sorted, unique Y-m-d strings. */ private static function currentStreak($completedDates): int { if ($completedDates->isEmpty()) { return 0; } $dateSet = $completedDates->flip(); $cursor = Carbon::today(); // A missed "today" doesn't break the streak until tomorrow - only walk // back from yesterday if today has no completed session yet. if (!$dateSet->has($cursor->toDateString())) { $cursor = $cursor->copy()->subDay(); } $streak = 0; while ($dateSet->has($cursor->toDateString())) { $streak++; $cursor = $cursor->copy()->subDay(); } return $streak; } /** * @param \Illuminate\Support\Collection $completedDates Sorted, unique Y-m-d strings. */ private static function longestStreak($completedDates): int { $longest = 0; $run = 0; $previous = null; foreach ($completedDates as $dateStr) { $date = Carbon::parse($dateStr); $run = ($previous && (int) abs($date->diffInDays($previous)) === 1) ? $run + 1 : 1; $longest = max($longest, $run); $previous = $date; } return $longest; } }