|
2 | 2 |
|
3 | 3 | import strawberry |
4 | 4 | import strawberry_django |
| 5 | +from django.db.models import IntegerField, Sum, Value |
| 6 | +from django.db.models.functions import Coalesce |
5 | 7 | from strawberry_django.filters import apply as apply_filters |
6 | 8 |
|
7 | 9 | from main.graphql.context import Info |
|
11 | 13 | # from .enums import TimeEntryDateFilterEnum |
12 | 14 | from .filters import ContractFilter, TaskFilter, TimeEntryFilter |
13 | 15 | from .orders import ContractOrder, TaskOrder, TimeEntryOrder |
14 | | -from .types import ContractType, TaskType, TimeEntryType |
| 16 | +from .types import ContractType, DailySummaryType, TaskType, TimeEntryType |
15 | 17 |
|
16 | 18 | # from django.db import models |
17 | 19 |
|
@@ -91,6 +93,79 @@ async def all_time_entries( |
91 | 93 | raise Exception(f"Try using filters. To much data to return (Row count: {count})") |
92 | 94 | return [time_entry async for time_entry in queryset] |
93 | 95 |
|
| 96 | + @strawberry_django.field( |
| 97 | + description="Return total minutes and target minutes per day for the user within the given date range.", |
| 98 | + ) |
| 99 | + async def daily_summary( |
| 100 | + self, |
| 101 | + info: Info, |
| 102 | + date_gte: datetime.date, |
| 103 | + date_lte: datetime.date, |
| 104 | + ) -> list[DailySummaryType]: |
| 105 | + from apps.common.models import Event |
| 106 | + from apps.journal.models import Journal |
| 107 | + |
| 108 | + from .models import TimeEntry |
| 109 | + |
| 110 | + # Recorded minutes per date |
| 111 | + qs = ( |
| 112 | + TimeEntry.objects.filter( |
| 113 | + user=info.context.request.user, |
| 114 | + date__gte=date_gte, |
| 115 | + date__lte=date_lte, |
| 116 | + ) |
| 117 | + .values("date") |
| 118 | + .annotate(total_minutes=Coalesce(Sum("duration"), Value(0), output_field=IntegerField())) |
| 119 | + .order_by("date") |
| 120 | + ) |
| 121 | + recorded_map: dict[datetime.date, int] = {} |
| 122 | + async for row in qs: |
| 123 | + recorded_map[row["date"]] = row["total_minutes"] |
| 124 | + |
| 125 | + # Holiday / non-working event dates (cached) |
| 126 | + non_working_dates = set(await Event.aget_relative_event_dates()) |
| 127 | + |
| 128 | + # User journal entries (leave + wfh) in range |
| 129 | + journal_qs = Journal.objects.filter( |
| 130 | + user=info.context.request.user, |
| 131 | + date__gte=date_gte, |
| 132 | + date__lte=date_lte, |
| 133 | + ).values("date", "leave_type", "wfh_type") |
| 134 | + journal_map: dict[datetime.date, dict] = {} |
| 135 | + async for row in journal_qs: |
| 136 | + journal_map[row["date"]] = row |
| 137 | + |
| 138 | + # Build one entry per day in the range |
| 139 | + result: list[DailySummaryType] = [] |
| 140 | + |
| 141 | + total_days = (date_lte - date_gte).days + 1 |
| 142 | + for offset in range(total_days): |
| 143 | + date = date_gte + datetime.timedelta(days=offset) |
| 144 | + journal = journal_map.get(date, {}) |
| 145 | + leave_type = journal.get("leave_type") |
| 146 | + wfh_type = journal.get("wfh_type") |
| 147 | + is_weekend = Event.is_weekend(date) |
| 148 | + is_holiday = not is_weekend and date in non_working_dates |
| 149 | + |
| 150 | + if is_weekend or is_holiday or leave_type == Journal.LeaveType.FULL: |
| 151 | + target = 0 |
| 152 | + elif leave_type in (Journal.LeaveType.FIRST_HALF, Journal.LeaveType.SECOND_HALF): |
| 153 | + target = int(3.5 * 60) |
| 154 | + else: |
| 155 | + target = 7 * 60 |
| 156 | + |
| 157 | + result.append( |
| 158 | + DailySummaryType( |
| 159 | + date=date, |
| 160 | + total_minutes=recorded_map.get(date, 0), |
| 161 | + target_minutes=target, |
| 162 | + is_holiday=is_holiday, |
| 163 | + leave_type=leave_type, |
| 164 | + wfh_type=wfh_type, |
| 165 | + ), |
| 166 | + ) |
| 167 | + return result |
| 168 | + |
94 | 169 | # Single ---------------------------- |
95 | 170 | @strawberry_django.field |
96 | 171 | async def contract(self, info: Info, pk: strawberry.ID) -> ContractType | None: |
|
0 commit comments