< Summary

Line coverage
100%
Covered lines: 381
Uncovered lines: 0
Coverable lines: 381
Total lines: 650
Line coverage: 100%
Branch coverage
100%
Covered branches: 82
Total branches: 82
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/ActivityService.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using Pomodoro.Web.Models;
 3using Pomodoro.Web.Services.Repositories;
 4
 5namespace Pomodoro.Web.Services;
 6
 7/// <summary>
 8/// Service for managing activity history records using IndexedDB
 9/// Stores unlimited history in IndexedDB but keeps a sliding window cache in memory
 10/// Implements ITimerEventSubscriber to handle timer completion events
 11/// </summary>
 12public partial class ActivityService : IActivityService, ITimerEventSubscriber
 13{
 14    private readonly IActivityRepository _activityRepository;
 15    private readonly ILogger<ActivityService> _logger;
 43916    private readonly object _cacheLock = new();
 43917    private List<ActivityRecord> _cachedActivities = new();
 18    private bool _isCacheLoaded;
 19
 20    /// <summary>
 21    /// Date-based cache for activities grouped by local date
 22    /// </summary>
 43923    private Dictionary<DateTime, List<ActivityRecord>> _activitiesByDate = new();
 24
 25    /// <summary>
 26    /// Cache for daily statistics (pomodoro count, focus minutes, break minutes)
 27    /// </summary>
 28    private readonly struct DailyStatsCache
 29    {
 25530        public int PomodoroCount { get; init; }
 21131        public int FocusMinutes { get; init; }
 23632        public int BreakMinutes { get; init; }
 33    }
 43934    private Dictionary<DateTime, DailyStatsCache> _dailyStatsCache = new();
 35
 36    /// <summary>
 37    /// Cache for time distribution data by date
 38    /// </summary>
 43939    private Dictionary<DateTime, Dictionary<string, int>> _timeDistributionCache = new();
 40
 41    public event Action? OnActivityChanged;
 42
 43943    public ActivityService(IActivityRepository activityRepository, ILogger<ActivityService> logger)
 43944    {
 43945        _activityRepository = activityRepository;
 43946        _logger = logger;
 43947    }
 48
 49    public async Task InitializeAsync()
 38350    {
 38351        await LoadCacheAsync();
 38352    }
 53
 54    /// <summary>
 55    /// Reloads all activity data from storage, clearing and rebuilding caches.
 56    /// Called after import operations to refresh in-memory data.
 57    /// </summary>
 58    public async Task ReloadAsync()
 2059    {
 2060        var activities = await _activityRepository.GetAllAsync();
 61
 2062        lock (_cacheLock)
 2063        {
 2064            _cachedActivities = activities.Take(Constants.Cache.MaxActivityCacheSize).ToList();
 2065            _isCacheLoaded = true;
 2066            ClearAllDerivedCaches();
 2067        }
 68
 2069        _logger.LogInformation("Reloaded {Count} activities from storage", _cachedActivities.Count);
 2070        OnActivityChanged?.Invoke();
 2071    }
 72
 73    private async Task LoadCacheAsync()
 38374    {
 39375        if (_isCacheLoaded) return;
 76
 37377        var activities = await _activityRepository.GetAllAsync();
 78
 79        // Apply cache size limit - only keep most recent activities in memory
 37380        lock (_cacheLock)
 37381        {
 37382            _cachedActivities = (activities ?? Enumerable.Empty<ActivityRecord>()).Take(Constants.Cache.MaxActivityCache
 37383            _isCacheLoaded = true;
 37384        }
 85
 37386        _logger.LogDebug(Constants.Messages.LogActivitiesLoadedFormat, _cachedActivities.Count, Constants.Cache.MaxActiv
 243787        foreach (var a in _cachedActivities.Take(5))
 65988        {
 65989            _logger.LogDebug(Constants.Messages.LogActivityDebugFormat, a.Type, a.CompletedAt, a.TaskName);
 65990        }
 38391    }
 92
 93    public List<ActivityRecord> GetTodayActivities()
 2094    {
 95        // Use local time for consistent date comparison (matches GetActivitiesForDate behavior)
 2096        var todayLocal = DateTime.Now.Date;
 2097        return GetActivitiesForDate(todayLocal);
 2098    }
 99
 100    /// <summary>
 101    /// Gets paged activities for a date range
 102    /// </summary>
 103    public async Task<List<ActivityRecord>> GetActivitiesPagedAsync(DateTime startDate, DateTime endDate, int skip = 0, 
 20104    {
 20105        return await _activityRepository.GetPagedAsync(startDate, endDate, skip, take);
 20106    }
 107
 108    /// <summary>
 109    /// Gets the total count of activities for a date range
 110    /// </summary>
 111    public async Task<int> GetActivityCountAsync(DateTime? startDate = null, DateTime? endDate = null)
 20112    {
 20113        return await _activityRepository.GetCountAsync(startDate, endDate);
 20114    }
 115
 116    public List<ActivityRecord> GetAllActivities()
 80117    {
 80118        lock (_cacheLock)
 80119        {
 80120            return _cachedActivities.ToList();
 121        }
 80122    }
 123
 124    /// <summary>
 125    /// Gets all activities for a specific date (in local time)
 126    /// Uses date-based cache for O(1) lookup on repeated access
 127    /// </summary>
 128    public List<ActivityRecord> GetActivitiesForDate(DateTime date)
 70129    {
 70130        var targetDate = date.Date;
 131
 70132        lock (_cacheLock)
 70133        {
 134            // Check cache first
 70135            if (_activitiesByDate.TryGetValue(targetDate, out var cached))
 10136            {
 10137                _logger.LogDebug("Cache hit for GetActivitiesForDate: {Date}", targetDate);
 10138                return cached;
 139            }
 140
 141            // Compute and cache
 60142            var result = _cachedActivities
 120143                .Where(a => a.CompletedAt.ToLocalTime().Date == targetDate)
 70144                .OrderByDescending(a => a.CompletedAt)
 60145                .ToList();
 146
 60147            _activitiesByDate[targetDate] = result;
 60148            _logger.LogDebug("Cache miss for GetActivitiesForDate: {Date}, cached {Count} activities", targetDate, resul
 60149            return result;
 150        }
 70151    }
 152
 153    /// <summary>
 154    /// Gets daily break minutes for a date range (in local time)
 155    /// Returns total minutes of both short and long breaks per day
 156    /// Uses date-based cache for improved performance
 157    /// </summary>
 158
 159    /// This is more efficient than computing each date separately when querying a range.
 160    /// </summary>
 161    /// <param name="dates">The dates to compute stats for</param>
 162    private void ComputeDailyStatsForRange(List<DateTime> dates)
 73163    {
 164        // Initialize stats for all dates
 73165        var statsByDate = new Dictionary<DateTime, (int PomodoroCount, int FocusMinutes, int BreakMinutes)>();
 541166        foreach (var date in dates)
 161167        {
 161168            statsByDate[date.Date] = (0, 0, 0);
 161169        }
 170
 171        // Single pass through all activities
 631172        foreach (var a in _cachedActivities)
 206173        {
 206174            var activityDate = a.CompletedAt.ToLocalTime().Date;
 175
 176            // Only process if this date is in our target list
 210177            if (!statsByDate.ContainsKey(activityDate)) continue;
 178
 202179            var stats = statsByDate[activityDate];
 202180            if (a.Type == SessionType.Pomodoro)
 110181            {
 110182                statsByDate[activityDate] = (stats.PomodoroCount + 1, stats.FocusMinutes + a.DurationMinutes, stats.Brea
 110183            }
 92184            else if (a.Type == SessionType.ShortBreak || a.Type == SessionType.LongBreak)
 92185            {
 92186                statsByDate[activityDate] = (stats.PomodoroCount, stats.FocusMinutes, stats.BreakMinutes + a.DurationMin
 92187            }
 202188        }
 189
 190        // Store in cache
 541191        foreach (var kvp in statsByDate)
 161192        {
 161193            _dailyStatsCache[kvp.Key] = new DailyStatsCache
 161194            {
 161195                PomodoroCount = kvp.Value.PomodoroCount,
 161196                FocusMinutes = kvp.Value.FocusMinutes,
 161197                BreakMinutes = kvp.Value.BreakMinutes
 161198            };
 161199        }
 200
 73201        _logger.LogDebug("Computed stats for {Count} dates in single pass", dates.Count);
 73202    }
 203
 204    /// <summary>
 205    /// Computes daily statistics for a single date using single-pass iteration
 206    /// </summary>
 207    private DailyStatsCache ComputeDailyStats(DateTime date)
 7208    {
 7209        var targetDate = date.Date;
 7210        var pomodoroCount = 0;
 7211        var focusMinutes = 0;
 7212        var breakMinutes = 0;
 213
 214        // Single-pass iteration for better performance
 61215        foreach (var a in _cachedActivities)
 20216        {
 24217            if (a.CompletedAt.ToLocalTime().Date != targetDate) continue;
 218
 16219            if (a.Type == SessionType.Pomodoro)
 9220            {
 9221                pomodoroCount++;
 9222                focusMinutes += a.DurationMinutes;
 9223            }
 7224            else if (a.Type == SessionType.ShortBreak || a.Type == SessionType.LongBreak)
 6225            {
 6226                breakMinutes += a.DurationMinutes;
 6227            }
 16228        }
 229
 7230        return new DailyStatsCache
 7231        {
 7232            PomodoroCount = pomodoroCount,
 7233            FocusMinutes = focusMinutes,
 7234            BreakMinutes = breakMinutes
 7235        };
 7236    }
 237
 238    /// <summary>
 239    /// Gets task pomodoro counts for a date range (in local time)
 240    /// </summary>
 241    public Dictionary<string, int> GetTaskPomodoroCounts(DateTime from, DateTime to)
 26242    {
 26243        var fromDate = from.Date;
 26244        var toDate = to.Date;
 245
 26246        lock (_cacheLock)
 26247        {
 26248            return _cachedActivities
 80249                .Where(a => a.Type == SessionType.Pomodoro &&
 80250                            a.CompletedAt.ToLocalTime().Date >= fromDate &&
 80251                            a.CompletedAt.ToLocalTime().Date <= toDate &&
 80252                            !string.IsNullOrWhiteSpace(a.TaskName))
 49253                .GroupBy(a => a.TaskName!)
 26254                .ToDictionary(
 38255                    g => g.Key,
 38256                    g => g.Count()
 26257                );
 258        }
 26259    }
 260
 261    /// <summary>
 262    /// Gets time distribution data for a specific date (in local time)
 263    /// Returns a dictionary with labels (task names or break types) as keys and minutes as values
 264    /// Uses date-based cache for O(1) lookup on repeated access
 265    /// </summary>
 266    public Dictionary<string, int> GetTimeDistribution(DateTime date)
 58267    {
 58268        var targetDate = date.Date;
 269
 58270        lock (_cacheLock)
 58271        {
 272            // Check cache first
 58273            if (_timeDistributionCache.TryGetValue(targetDate, out var cached))
 10274            {
 10275                _logger.LogDebug("Cache hit for GetTimeDistribution: {Date}", targetDate);
 10276                return cached;
 277            }
 278
 279            // Compute and cache
 48280            var dayActivities = _cachedActivities
 115281                .Where(a => a.CompletedAt.ToLocalTime().Date == targetDate)
 48282                .ToList();
 283
 48284            var result = new Dictionary<string, int>();
 285
 286            // Group pomodoro sessions by task name
 48287            var pomodoroByTask = dayActivities
 105288                .Where(a => a.Type == SessionType.Pomodoro)
 76289                .GroupBy(a => a.TaskName ?? Constants.Activity.FocusTimeLabel)
 48290                .ToDictionary(
 62291                    g => g.Key,
 138292                    g => g.Sum(a => a.DurationMinutes)
 48293                );
 294
 295            // Add task times to result
 268296            foreach (var kvp in pomodoroByTask)
 62297            {
 62298                result[kvp.Key] = kvp.Value;
 62299            }
 300
 301            // Aggregate short breaks
 48302            var shortBreakMinutes = dayActivities
 105303                .Where(a => a.Type == SessionType.ShortBreak)
 64304                .Sum(a => a.DurationMinutes);
 305
 48306            if (shortBreakMinutes > 0)
 15307            {
 15308                result[Constants.Activity.ShortBreaksLabel] = shortBreakMinutes;
 15309            }
 310
 311            // Aggregate long breaks
 48312            var longBreakMinutes = dayActivities
 105313                .Where(a => a.Type == SessionType.LongBreak)
 61314                .Sum(a => a.DurationMinutes);
 315
 48316            if (longBreakMinutes > 0)
 13317            {
 13318                result[Constants.Activity.LongBreaksLabel] = longBreakMinutes;
 13319            }
 320
 48321            _timeDistributionCache[targetDate] = result;
 48322            _logger.LogDebug("Cache miss for GetTimeDistribution: {Date}, cached {Count} entries", targetDate, result.Co
 48323            return result;
 324        }
 58325    }
 326
 327    /// <summary>
 328    /// Gets weekly statistics for a given week start date
 329    /// </summary>
 330    public async Task<WeeklyStats> GetWeeklyStatsAsync(DateTime weekStartDate)
 24331    {
 332        try
 24333        {
 334            // Week starts on Saturday (matches History page calculation)
 335            // DayOfWeek: Sun=0, Mon=1, Tue=2, Wed=3, Thu=4, Fri=5, Sat=6
 336            // Using AddDays(7) because IDBKeyRange.bound() is inclusive, so end at Saturday midnight includes all Frida
 24337            var weekStart = weekStartDate.Date;
 24338            var weekEnd = weekStart.AddDays(7); // Saturday to Saturday midnight (includes Saturday through Friday)
 24339            var previousWeekStart = weekStart.AddDays(-7);
 340
 341            // Get activities for current week from repository (to handle large datasets)
 24342            var currentWeekActivities = await _activityRepository.GetByDateRangeAsync(weekStart, weekEnd);
 23343            var previousWeekActivities = await _activityRepository.GetByDateRangeAsync(previousWeekStart, weekStart);
 344
 345        // Return zero stats if no activities for the week
 22346        if (!currentWeekActivities.Any())
 10347        {
 10348            return new WeeklyStats
 10349            {
 10350                TotalFocusMinutes = 0,
 10351                TotalPomodoroCount = 0,
 10352                UniqueTasksWorkedOn = 0,
 10353                DailyAverageMinutes = 0,
 10354                MostProductiveDay = DayOfWeek.Monday,
 10355                PreviousWeekFocusMinutes = 0,
 10356                WeekOverWeekChange = 0
 10357            };
 358        }
 359
 360        // Calculate current week statistics
 361        var pomodoroActivities = currentWeekActivities.Where(a => a.Type == SessionType.Pomodoro).ToList();
 362        var totalFocusMinutes = pomodoroActivities.Sum(a => a.DurationMinutes);
 12363        var totalPomodoroCount = pomodoroActivities.Count;
 12364        var uniqueTasks = pomodoroActivities
 365            .Where(a => !string.IsNullOrEmpty(a.TaskName))
 366            .Select(a => a.TaskName)
 12367            .Distinct()
 12368            .Count();
 369
 370        // Calculate daily average (divide by 7 for full week, or actual days with activity)
 12371        var daysWithActivity = currentWeekActivities
 372            .Select(a => a.CompletedAt.ToLocalTime().Date)
 12373            .Distinct()
 12374            .Count();
 12375        var dailyAverageMinutes = (double)totalFocusMinutes / daysWithActivity;
 376
 377        // Find most productive day
 12378        var dayTotals = pomodoroActivities
 379            .GroupBy(a => a.CompletedAt.ToLocalTime().DayOfWeek)
 380            .Select(g => new { Day = g.Key, Minutes = g.Sum(a => a.DurationMinutes) })
 381            .OrderByDescending(g => g.Minutes)
 12382            .FirstOrDefault();
 12383        var mostProductiveDay = dayTotals?.Day ?? DayOfWeek.Monday;
 384
 385        // Calculate previous week focus minutes
 12386        var previousWeekFocusMinutes = previousWeekActivities
 387            .Where(a => a.Type == SessionType.Pomodoro)
 388            .Sum(a => a.DurationMinutes);
 389
 390        // Calculate week-over-week change percentage
 12391        double weekOverWeekChange = 0;
 12392        if (previousWeekFocusMinutes > 0)
 1393        {
 1394            weekOverWeekChange = ((double)(totalFocusMinutes - previousWeekFocusMinutes) / previousWeekFocusMinutes) * 1
 1395        }
 11396        else if (totalFocusMinutes > 0)
 10397        {
 398            // Previous week had no data, this week has data - treat as 100% increase
 10399            weekOverWeekChange = 100;
 10400        }
 401
 12402        return new WeeklyStats
 12403        {
 12404            TotalFocusMinutes = totalFocusMinutes,
 12405            TotalPomodoroCount = totalPomodoroCount,
 12406            UniqueTasksWorkedOn = uniqueTasks,
 12407            DailyAverageMinutes = dailyAverageMinutes,
 12408            MostProductiveDay = mostProductiveDay,
 12409            PreviousWeekFocusMinutes = previousWeekFocusMinutes,
 12410            WeekOverWeekChange = weekOverWeekChange
 12411        };
 412        }
 2413        catch (Exception ex)
 2414        {
 2415            _logger.LogError(ex, "Error calculating weekly stats for week starting {WeekStartDate}", weekStartDate);
 2416            return new WeeklyStats
 2417            {
 2418                TotalFocusMinutes = 0,
 2419                TotalPomodoroCount = 0,
 2420                UniqueTasksWorkedOn = 0,
 2421                DailyAverageMinutes = 0,
 2422                MostProductiveDay = DayOfWeek.Monday,
 2423                PreviousWeekFocusMinutes = 0,
 2424                WeekOverWeekChange = 0
 2425            };
 426        }
 24427    }
 428
 429    public async Task AddActivityAsync(ActivityRecord activity)
 70430    {
 431        // Add to cache with thread safety and trim if needed
 70432        lock (_cacheLock)
 70433        {
 70434            _cachedActivities.Insert(0, activity);
 435
 436            // Trim cache if it exceeds max size (remove oldest entries from end)
 437            // Track dates of removed activities to invalidate their caches
 70438            var datesToInvalidate = new HashSet<DateTime>();
 439
 80440            while (_cachedActivities.Count > Constants.Cache.MaxActivityCacheSize)
 10441            {
 10442                var removed = _cachedActivities[^1];
 10443                datesToInvalidate.Add(removed.CompletedAt.ToLocalTime().Date);
 10444                _cachedActivities.RemoveAt(_cachedActivities.Count - 1);
 10445            }
 446
 447            // Invalidate caches for all affected dates (removed activities + new activity)
 230448            foreach (var date in datesToInvalidate)
 10449            {
 10450                InvalidateDateCache(date);
 10451            }
 452
 453            // Also invalidate the new activity's date
 70454            InvalidateDateCache(activity.CompletedAt.ToLocalTime().Date);
 70455        }
 456
 70457        _logger.LogDebug(Constants.Messages.LogAddedActivityFormat, activity.Type, activity.CompletedAt, _cachedActiviti
 458
 459        // Save to repository
 70460        await _activityRepository.SaveAsync(activity);
 461
 70462        OnActivityChanged?.Invoke();
 70463    }
 464
 465    public async Task ClearAllActivitiesAsync()
 30466    {
 30467        lock (_cacheLock)
 30468        {
 30469            _cachedActivities.Clear();
 30470            ClearAllDerivedCaches();
 30471        }
 472
 30473        await _activityRepository.ClearAllAsync();
 30474        OnActivityChanged?.Invoke();
 30475    }
 476
 477    /// <summary>
 478    /// Gets activities for a date range directly from IndexedDB
 479    /// Useful for large datasets where caching everything isn't practical
 480    /// </summary>
 481    public async Task<List<ActivityRecord>> GetActivitiesByDateRangeAsync(DateTime from, DateTime to)
 10482    {
 10483        return await _activityRepository.GetByDateRangeAsync(from, to);
 10484    }
 485
 486    /// <summary>
 487    /// Handles timer completion events from ITimerEventSubscriber
 488    /// Creates an activity record for the completed session
 489    /// </summary>
 490    public async Task HandleTimerCompletedAsync(TimerCompletedEventArgs args)
 20491    {
 20492        var activity = new ActivityRecord
 20493        {
 20494            Id = Guid.NewGuid(),
 20495            Type = args.SessionType,
 20496            TaskId = args.TaskId,
 20497            TaskName = args.TaskName,
 20498            CompletedAt = args.CompletedAt,
 20499            DurationMinutes = args.DurationMinutes,
 20500            WasCompleted = args.WasCompleted
 20501        };
 502
 20503        await AddActivityAsync(activity);
 20504    }
 505
 506    #region Cache Management
 507
 508    /// <summary>
 509    /// Invalidates all cached data for a specific date
 510    /// </summary>
 511    private void InvalidateDateCache(DateTime date)
 92512    {
 92513        var localDate = date.Date;
 92514        _activitiesByDate.Remove(localDate);
 92515        _dailyStatsCache.Remove(localDate);
 92516        _timeDistributionCache.Remove(localDate);
 92517        _logger.LogDebug("Invalidated cache for date: {Date}", localDate);
 92518    }
 519
 520    /// <summary>
 521    /// Clears all derived caches (called when primary cache is cleared)
 522    /// </summary>
 523    private void ClearAllDerivedCaches()
 50524    {
 50525        _activitiesByDate.Clear();
 50526        _dailyStatsCache.Clear();
 50527        _timeDistributionCache.Clear();
 50528        _logger.LogDebug("Cleared all derived caches");
 50529    }
 530
 531    /// <summary>
 532    /// Gets cache statistics for debugging/monitoring
 533    /// </summary>
 534    public (int ActivityCount, int DatesCached, int StatsCached, int DistributionCached) GetCacheStatistics()
 10535    {
 10536        lock (_cacheLock)
 10537        {
 10538            return (
 10539                _cachedActivities.Count,
 10540                _activitiesByDate.Count,
 10541                _dailyStatsCache.Count,
 10542                _timeDistributionCache.Count
 10543            );
 544        }
 10545    }
 546
 547    /// <summary>
 548    /// Invalidates cached data for a specific activity by its date.
 549    /// Useful for future extensibility when activities can be modified or deleted individually.
 550    /// </summary>
 551    /// <param name="activityId">The ID of the activity that was modified/deleted</param>
 552    public void InvalidateCacheForActivity(Guid activityId)
 14553    {
 14554        lock (_cacheLock)
 14555        {
 556            // Find the activity in cache to get its date
 26557            var activity = _cachedActivities.FirstOrDefault(a => a.Id == activityId);
 14558            if (activity != null)
 12559            {
 12560                InvalidateDateCache(activity.CompletedAt.ToLocalTime().Date);
 12561                _logger.LogDebug("Invalidated cache for activity {ActivityId} on date {Date}", activityId, activity.Comp
 12562            }
 14563        }
 14564    }
 565
 566    /// <summary>
 567    /// Invalidates cached data for a date range.
 568    /// Useful for bulk operations that affect multiple days.
 569    /// </summary>
 570    /// <param name="from">Start date (inclusive)</param>
 571    /// <param name="to">End date (inclusive)</param>
 572    public void InvalidateCacheForDateRange(DateTime from, DateTime to)
 16573    {
 16574        lock (_cacheLock)
 16575        {
 16576            var count = 0;
 910577            for (var date = from.Date; date <= to.Date; date = date.AddDays(1))
 439578            {
 439579                _activitiesByDate.Remove(date);
 439580                _dailyStatsCache.Remove(date);
 439581                _timeDistributionCache.Remove(date);
 439582                count++;
 439583            }
 16584            _logger.LogDebug("Invalidated cache for {Count} dates from {From} to {To}", count, from.Date, to.Date);
 16585        }
 16586    }
 587
 588    #endregion
 589}

/home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/ActivityService.DailyStats.cs

#LineLine coverage
 1using Pomodoro.Web.Models;
 2
 3namespace Pomodoro.Web.Services;
 4
 5public partial class ActivityService
 6{
 7    private delegate int DailyStatsSelector(DailyStatsCache stats);
 8
 9    private Dictionary<DateTime, int> GetDailyStats(DateTime from, DateTime to, DailyStatsSelector selector)
 8710    {
 8711        var fromDate = from.Date;
 8712        var toDate = to.Date;
 13
 8714        lock (_cacheLock)
 8715        {
 8716            var result = new Dictionary<DateTime, int>();
 8717            var uncachedDates = new List<DateTime>();
 18
 52819            for (var date = fromDate; date <= toDate; date = date.AddDays(1))
 17720            {
 17721                if (_dailyStatsCache.TryGetValue(date, out var cached))
 1622                {
 1623                    var value = selector(cached);
 1624                    if (value > 0)
 1225                    {
 1226                        result[date] = value;
 1227                    }
 1628                }
 29                else
 16130                {
 16131                    uncachedDates.Add(date);
 16132                }
 17733            }
 34
 8735            if (uncachedDates.Count > 0)
 7336            {
 7337                ComputeDailyStatsForRange(uncachedDates);
 38
 54139                foreach (var date in uncachedDates)
 16140                {
 16141                    var value = selector(_dailyStatsCache[date]);
 16142                    if (value > 0)
 9143                    {
 9144                        result[date] = value;
 9145                    }
 16146                }
 7347            }
 48
 8749            return result;
 50        }
 8751    }
 52
 53    public Dictionary<DateTime, int> GetDailyPomodoroCounts(DateTime from, DateTime to)
 11254        => GetDailyStats(from, to, static s => s.PomodoroCount);
 55
 56    public Dictionary<DateTime, int> GetDailyFocusMinutes(DateTime from, DateTime to)
 6857        => GetDailyStats(from, to, static s => s.FocusMinutes);
 58
 59    public Dictionary<DateTime, int> GetDailyBreakMinutes(DateTime from, DateTime to)
 8460        => GetDailyStats(from, to, static s => s.BreakMinutes);
 61}