from datetime import date from typing import Optional import strawberry from strawberry import ID from django.db.models import Prefetch from asgiref.sync import sync_to_async from core.graphql.types.dashboard import ( AdminDashboardData, TeamDashboardData, CustomerDashboardData, ) from core.models.service import Service from core.models.project import Project from core.models.invoice import Invoice from core.models.report import Report from core.models.scope_template import ScopeTemplate, AreaTemplate, TaskTemplate from core.models.project_scope_template import ( ProjectScopeTemplate, ProjectAreaTemplate, ProjectTaskTemplate, ) def parse_month_range(month: str) -> tuple[date, date]: """Parse a month string like '2024-01' into start and end dates.""" year, month_num = map(int, month.split('-')) start = date(year, month_num, 1) # Calculate end of month if month_num == 12: end = date(year + 1, 1, 1) else: end = date(year, month_num + 1, 1) # End is exclusive, so subtract one day for inclusive range from datetime import timedelta end = end - timedelta(days=1) return start, end def _fetch_admin_dashboard_sync( start: date, end: date, invoice_status: Optional[str], ) -> AdminDashboardData: """Synchronous database fetching for admin dashboard.""" # Services - optimized with prefetch for team_members services = list( Service.objects .filter(date__gte=start, date__lte=end) .select_related('account_address', 'account_address__account') .prefetch_related('team_members') .order_by('date', 'id') ) # Projects - optimized with prefetch for team_members projects = list( Project.objects .filter(date__gte=start, date__lte=end) .select_related('account_address', 'account_address__account', 'customer') .prefetch_related('team_members') .order_by('date', 'id') ) # Invoices - show all (pages need full list, not month-filtered) invoices_qs = Invoice.objects.select_related('customer') if invoice_status: invoices_qs = invoices_qs.filter(status=invoice_status) invoices = list(invoices_qs.order_by('-date', '-id')) # Reports - show all (pages need full list, not month-filtered) reports = list( Report.objects .select_related('team_member') .order_by('-date', '-id') ) # Service Scope Templates - with nested areas and tasks prefetched task_prefetch = Prefetch( 'task_templates', queryset=TaskTemplate.objects.order_by('order', 'id') ) area_prefetch = Prefetch( 'area_templates', queryset=AreaTemplate.objects.prefetch_related(task_prefetch).order_by('order', 'name') ) service_scope_templates = list( ScopeTemplate.objects .prefetch_related(area_prefetch) .order_by('name') ) # Project Scope Templates - with nested categories and tasks prefetched project_task_prefetch = Prefetch( 'task_templates', queryset=ProjectTaskTemplate.objects.order_by('order', 'id') ) category_prefetch = Prefetch( 'category_templates', queryset=ProjectAreaTemplate.objects.prefetch_related(project_task_prefetch).order_by('order', 'name') ) project_scope_templates = list( ProjectScopeTemplate.objects .prefetch_related(category_prefetch) .order_by('name') ) return AdminDashboardData( services=services, projects=projects, invoices=invoices, reports=reports, service_scope_templates=service_scope_templates, project_scope_templates=project_scope_templates, ) def _fetch_team_dashboard_sync( team_profile_id: str, start: date, end: date, ) -> TeamDashboardData: """Synchronous database fetching for team dashboard.""" # Services assigned to this team member services = list( Service.objects .filter( team_members__id=team_profile_id, date__gte=start, date__lte=end ) .select_related('account_address', 'account_address__account') .prefetch_related('team_members') .order_by('date', 'id') ) # Projects assigned to this team member projects = list( Project.objects .filter( team_members__id=team_profile_id, date__gte=start, date__lte=end ) .select_related('account_address', 'account_address__account', 'customer') .prefetch_related('team_members') .order_by('date', 'id') ) # Reports for this team member reports = list( Report.objects .filter( team_member_id=team_profile_id, date__gte=start, date__lte=end ) .select_related('team_member') .order_by('-date', '-id') ) return TeamDashboardData( services=services, projects=projects, reports=reports, ) def _fetch_customer_dashboard_sync( customer_id: str, ) -> CustomerDashboardData: """Synchronous database fetching for customer dashboard.""" # Services for customer's accounts services = list( Service.objects .filter(account_address__account__customer_id=customer_id) .select_related('account_address', 'account_address__account') .prefetch_related('team_members') .order_by('-date', '-id')[:100] # Limit for performance ) # Projects for customer projects = list( Project.objects .filter(customer_id=customer_id) .select_related('account_address', 'account_address__account', 'customer') .prefetch_related('team_members') .order_by('-date', '-id')[:100] # Limit for performance ) # Invoices for customer invoices = list( Invoice.objects .filter(customer_id=customer_id) .select_related('customer') .order_by('-date', '-id')[:100] # Limit for performance ) return CustomerDashboardData( services=services, projects=projects, invoices=invoices, ) @strawberry.type class Query: @strawberry.field( name="adminDashboard", description="Consolidated dashboard data for admin/team leader users. " "Returns all services, projects, invoices, reports, and scope templates " "for the given month in a single optimized query." ) async def admin_dashboard( self, info, month: str, invoice_status: Optional[str] = None, ) -> AdminDashboardData: """Fetch all admin dashboard data in a single optimized query. Args: month: Month string in format 'YYYY-MM' (e.g., '2024-01') invoice_status: Optional invoice status filter (e.g., 'SENT', 'PAID') Returns: AdminDashboardData with all dashboard entities """ start, end = parse_month_range(month) return await sync_to_async(_fetch_admin_dashboard_sync)(start, end, invoice_status) @strawberry.field( name="teamDashboard", description="Consolidated dashboard data for team member users. " "Returns services and projects assigned to the requesting user." ) async def team_dashboard( self, info, team_profile_id: ID, month: str, ) -> TeamDashboardData: """Fetch all team dashboard data in a single optimized query. Args: team_profile_id: The team member's profile ID month: Month string in format 'YYYY-MM' (e.g., '2024-01') Returns: TeamDashboardData with services and projects for the team member """ start, end = parse_month_range(month) return await sync_to_async(_fetch_team_dashboard_sync)(team_profile_id, start, end) @strawberry.field( name="customerDashboard", description="Consolidated dashboard data for customer users. " "Returns services, projects, and invoices for the customer." ) async def customer_dashboard( self, info, customer_id: ID, ) -> CustomerDashboardData: """Fetch all customer dashboard data in a single optimized query. Args: customer_id: The customer's profile ID Returns: CustomerDashboardData with services, projects, and invoices """ return await sync_to_async(_fetch_customer_dashboard_sync)(customer_id)