From a348f31a9eedf9c9a9771be2f1b962e1627556f2 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sun, 7 Dec 2025 12:19:11 -0600 Subject: [PATCH] Add per-residence overdue_count to API response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds overdueCount field to each residence in my-residences endpoint, enabling the mobile app to show pulsing icons on individual residence cards that have overdue tasks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/dto/responses/residence.go | 49 +++++++++++++------------- internal/repositories/task_repo.go | 34 ++++++++++++++++++ internal/services/residence_service.go | 10 ++++++ 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/internal/dto/responses/residence.go b/internal/dto/responses/residence.go index cb6c2aa..1317202 100644 --- a/internal/dto/responses/residence.go +++ b/internal/dto/responses/residence.go @@ -25,31 +25,32 @@ type ResidenceUserResponse struct { // ResidenceResponse represents a residence in the API response type ResidenceResponse struct { - ID uint `json:"id"` - OwnerID uint `json:"owner_id"` - Owner *ResidenceUserResponse `json:"owner,omitempty"` + ID uint `json:"id"` + OwnerID uint `json:"owner_id"` + Owner *ResidenceUserResponse `json:"owner,omitempty"` Users []ResidenceUserResponse `json:"users,omitempty"` - Name string `json:"name"` - PropertyTypeID *uint `json:"property_type_id"` - PropertyType *ResidenceTypeResponse `json:"property_type,omitempty"` - StreetAddress string `json:"street_address"` - ApartmentUnit string `json:"apartment_unit"` - City string `json:"city"` - StateProvince string `json:"state_province"` - PostalCode string `json:"postal_code"` - Country string `json:"country"` - Bedrooms *int `json:"bedrooms"` - Bathrooms *decimal.Decimal `json:"bathrooms"` - SquareFootage *int `json:"square_footage"` - LotSize *decimal.Decimal `json:"lot_size"` - YearBuilt *int `json:"year_built"` - Description string `json:"description"` - PurchaseDate *time.Time `json:"purchase_date"` - PurchasePrice *decimal.Decimal `json:"purchase_price"` - IsPrimary bool `json:"is_primary"` - IsActive bool `json:"is_active"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + PropertyTypeID *uint `json:"property_type_id"` + PropertyType *ResidenceTypeResponse `json:"property_type,omitempty"` + StreetAddress string `json:"street_address"` + ApartmentUnit string `json:"apartment_unit"` + City string `json:"city"` + StateProvince string `json:"state_province"` + PostalCode string `json:"postal_code"` + Country string `json:"country"` + Bedrooms *int `json:"bedrooms"` + Bathrooms *decimal.Decimal `json:"bathrooms"` + SquareFootage *int `json:"square_footage"` + LotSize *decimal.Decimal `json:"lot_size"` + YearBuilt *int `json:"year_built"` + Description string `json:"description"` + PurchaseDate *time.Time `json:"purchase_date"` + PurchasePrice *decimal.Decimal `json:"purchase_price"` + IsPrimary bool `json:"is_primary"` + IsActive bool `json:"is_active"` + OverdueCount int `json:"overdue_count"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TotalSummary represents summary statistics for all residences diff --git a/internal/repositories/task_repo.go b/internal/repositories/task_repo.go index 2f61a92..8f66c72 100644 --- a/internal/repositories/task_repo.go +++ b/internal/repositories/task_repo.go @@ -502,3 +502,37 @@ func (r *TaskRepository) GetTaskStatistics(residenceIDs []uint) (*TaskStatistics TasksDueNextMonth: int(tasksDueNextMonth), }, nil } + +// GetOverdueCountByResidence returns a map of residence ID to overdue task count. +// Uses the task.scopes package for consistent filtering logic. +func (r *TaskRepository) GetOverdueCountByResidence(residenceIDs []uint) (map[uint]int, error) { + if len(residenceIDs) == 0 { + return map[uint]int{}, nil + } + + now := time.Now().UTC() + + // Query to get overdue count grouped by residence + type result struct { + ResidenceID uint + Count int64 + } + var results []result + + err := r.db.Model(&models.Task{}). + Select("residence_id, COUNT(*) as count"). + Scopes(task.ScopeForResidences(residenceIDs), task.ScopeOverdue(now)). + Group("residence_id"). + Scan(&results).Error + if err != nil { + return nil, err + } + + // Convert to map + countMap := make(map[uint]int) + for _, r := range results { + countMap[r.ResidenceID] = int(r.Count) + } + + return countMap, nil +} diff --git a/internal/services/residence_service.go b/internal/services/residence_service.go index 1cd382a..01618d5 100644 --- a/internal/services/residence_service.go +++ b/internal/services/residence_service.go @@ -113,6 +113,16 @@ func (s *ResidenceService) GetMyResidences(userID uint) (*responses.MyResidences summary.TasksDueNextWeek = stats.TasksDueNextWeek summary.TasksDueNextMonth = stats.TasksDueNextMonth } + + // Get per-residence overdue counts + overdueCounts, err := s.taskRepo.GetOverdueCountByResidence(residenceIDs) + if err == nil && overdueCounts != nil { + for i := range residenceResponses { + if count, ok := overdueCounts[residenceResponses[i].ID]; ok { + residenceResponses[i].OverdueCount = count + } + } + } } return &responses.MyResidencesResponse{