diff --git a/apps/reports/services/aggregation.py b/apps/reports/services/aggregation.py index 4137930..a510ac9 100644 --- a/apps/reports/services/aggregation.py +++ b/apps/reports/services/aggregation.py @@ -387,31 +387,57 @@ def build_chart_report(actor, raw_filters) -> dict: filters = load_report_filters(actor, raw_filters) entries = list(_base_queryset(filters)) summary = _summary_from_entries(entries) - buckets: dict[str, dict] = {} + grouped_entries: dict[str | None, list[TimeEntry]] = defaultdict(list) + if filters.is_workspace_scope and not filters.user_id: + for entry in entries: + grouped_entries[str(entry.user_id)].append(entry) + else: + grouped_entries[filters.user_id] = entries - for entry in entries: - local_start = _localize_datetime(entry.start_time) - bucket_id, bucket_date = _bucket_key(filters, local_start) - bucket = buckets.setdefault( - bucket_id, + serialized_series = [] + for _, series_entries in sorted( + grouped_entries.items(), + key=lambda item: _user_display(item[1][0].user).lower() if item[1] else "", + ): + if not series_entries: + continue + + buckets: dict[str, dict] = {} + for entry in series_entries: + local_start = _localize_datetime(entry.start_time) + bucket_id, bucket_date = _bucket_key(filters, local_start) + bucket = buckets.setdefault( + bucket_id, + { + "bucket_key": bucket_id, + "bucket_label": _bucket_label(filters, bucket_date), + "total_seconds": 0, + "total_duration": "00:00:00", + }, + ) + bucket["total_seconds"] += get_entry_duration_seconds(entry) + + serialized_buckets = [] + for bucket in sorted(buckets.values(), key=lambda item: item["bucket_key"]): + bucket["total_duration"] = _format_duration_seconds(bucket["total_seconds"]) + serialized_buckets.append(bucket) + + user = series_entries[0].user + serialized_series.append( { - "bucket_key": bucket_id, - "bucket_label": _bucket_label(filters, bucket_date), - "total_seconds": 0, - "total_duration": "00:00:00", - }, + "user": { + "id": str(user.id), + "name": _user_display(user), + "mobile": user.mobile, + }, + "buckets": serialized_buckets, + } ) - bucket["total_seconds"] += get_entry_duration_seconds(entry) - - serialized_buckets = [] - for bucket in sorted(buckets.values(), key=lambda item: item["bucket_key"]): - bucket["total_duration"] = _format_duration_seconds(bucket["total_seconds"]) - serialized_buckets.append(bucket) return { "scope": _scope_payload(filters), "summary": summary, - "buckets": serialized_buckets, + "series": serialized_series, } diff --git a/apps/reports/tests/test_views.py b/apps/reports/tests/test_views.py index 27587c2..07e8d8f 100644 --- a/apps/reports/tests/test_views.py +++ b/apps/reports/tests/test_views.py @@ -100,6 +100,28 @@ class ReportViewTests(APITestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.data["summary"]["total_duration"], "01:00:00") + self.assertEqual(len(response.data["series"]), 1) + self.assertEqual(response.data["series"][0]["user"]["id"], str(self.member.id)) + + def test_admin_chart_without_user_filter_returns_series_for_all_users(self): + self.client.force_authenticate(user=self.admin) + + with patch( + "apps.reports.services.aggregation.timezone.localdate", + return_value=date(2026, 4, 20), + ): + response = self.client.get( + "/api/reports/chart/", + {"workspace": str(self.workspace.id), "period": "this_month"}, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["summary"]["total_duration"], "03:00:00") + self.assertEqual(len(response.data["series"]), 2) + self.assertEqual( + {series["user"]["id"] for series in response.data["series"]}, + {str(self.owner.id), str(self.member.id)}, + ) def test_admin_can_request_combined_table_report(self): self.client.force_authenticate(user=self.admin)